okstra 0.146.0 → 0.147.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 (65) hide show
  1. package/README.md +2 -2
  2. package/docs/architecture/storage-model.md +8 -7
  3. package/docs/architecture.md +18 -12
  4. package/docs/cli.md +3 -3
  5. package/docs/project-structure-overview.md +16 -14
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/workers/report-writer-worker.md +10 -10
  9. package/runtime/bin/okstra-render-final-report.py +4 -4
  10. package/runtime/bin/okstra-render-report-views.py +100 -12
  11. package/runtime/bin/okstra-trace-cleanup.sh +13 -9
  12. package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
  13. package/runtime/prompts/lead/report-writer.md +15 -11
  14. package/runtime/prompts/profiles/_common-contract.md +13 -7
  15. package/runtime/prompts/profiles/improvement-discovery.md +3 -1
  16. package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
  17. package/runtime/python/okstra_ctl/render_final_report.py +136 -28
  18. package/runtime/python/okstra_ctl/report_contract.py +124 -0
  19. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  20. package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
  21. package/runtime/python/okstra_ctl/report_html/common.py +50 -0
  22. package/runtime/python/okstra_ctl/report_html/models.py +59 -0
  23. package/runtime/python/okstra_ctl/report_html/render.py +69 -0
  24. package/runtime/python/okstra_ctl/report_html/router.py +40 -0
  25. package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
  26. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +38 -0
  27. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +48 -0
  28. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +38 -0
  29. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +46 -0
  30. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +46 -0
  31. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +102 -0
  32. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +42 -0
  33. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
  34. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +53 -0
  35. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +54 -0
  36. package/runtime/python/okstra_ctl/report_html/visualizations.py +113 -0
  37. package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
  38. package/runtime/python/okstra_ctl/report_views.py +15 -43
  39. package/runtime/python/okstra_ctl/run.py +12 -6
  40. package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
  41. package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
  42. package/runtime/templates/reports/final-report-v2.template.md +66 -0
  43. package/runtime/templates/reports/html/assets/base.css +38 -0
  44. package/runtime/templates/reports/html/assets/base.js +5 -0
  45. package/runtime/templates/reports/html/base.template.html +65 -0
  46. package/runtime/templates/reports/html/macros/forms.html +47 -0
  47. package/runtime/templates/reports/html/macros/layout.html +19 -0
  48. package/runtime/templates/reports/html/macros/visualizations.html +17 -0
  49. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
  50. package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
  51. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
  52. package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
  53. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
  54. package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
  55. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
  56. package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
  57. package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
  58. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
  59. package/runtime/validators/validate-report-views.py +86 -4
  60. package/runtime/validators/validate-run.py +62 -9
  61. package/runtime/validators/validate_improvement_report.py +55 -0
  62. package/src/commands/lifecycle/install.mjs +18 -13
  63. package/src/commands/report/finalize.mjs +2 -3
  64. package/src/commands/report/render-final-report.mjs +4 -2
  65. package/src/commands/report/render-views.mjs +8 -8
@@ -35,6 +35,11 @@ from pathlib import Path
35
35
  from typing import Iterable, Optional
36
36
 
37
37
  from .final_report_paths import final_report_data_path
38
+ from .report_html.view_models.implementation_planning import (
39
+ PlanApprovalState,
40
+ plan_approval_state,
41
+ resolve_recommended_option,
42
+ )
38
43
  from .report_view_artifacts import html_view_path, user_responses_dir_for_report
39
44
 
40
45
 
@@ -1142,16 +1147,7 @@ def report_has_clarification_items(src_md: str) -> bool:
1142
1147
  )
1143
1148
 
1144
1149
 
1145
- @dataclass(frozen=True)
1146
- class PlanApprovalContext:
1147
- """Plan Approval 위젯 렌더 입력. ``disabled_reason`` 이 비어 있지 않으면
1148
- 위젯은 disabled 로 렌더된다 (run-prep 승인 게이트와 동일한 fail-closed 기준).
1149
- ``blocker_ids`` 는 §1 미해소 승인-차단 항목 ID 목록 — 화면 안내가 사용자에게
1150
- '어느 항목을 답해야 하는지' 를 보여주는 데 쓴다 (unreadable 사유면 빈 튜플)."""
1151
- option_names: tuple[str, ...]
1152
- recommended_option: str
1153
- disabled_reason: str
1154
- blocker_ids: tuple[str, ...]
1150
+ PlanApprovalContext = PlanApprovalState
1155
1151
 
1156
1152
 
1157
1153
  def _load_report_data(src_md_path: Path) -> dict | None:
@@ -1210,31 +1206,21 @@ def plan_approval_context(
1210
1206
  data = _load_report_data(src_md_path)
1211
1207
  if data is None:
1212
1208
  return None
1213
- planning = data.get("implementationPlanning")
1214
- if not isinstance(planning, dict):
1209
+ state = plan_approval_state(data)
1210
+ if state is None:
1215
1211
  return None
1216
- names = tuple(
1217
- c.get("name", "")
1218
- for c in (planning.get("optionCandidates") or [])
1219
- if isinstance(c, dict) and c.get("name")
1220
- )
1221
- if not names:
1222
- return None
1223
- rec = planning.get("recommendedOption")
1224
- rec_name = rec.get("name") or "" if isinstance(rec, dict) else ""
1225
- recommended = _resolve_recommended_option(rec_name, names)
1226
1212
  scan = scan_approval_gate(src_text)
1227
- blocker_ids: tuple[str, ...] = ()
1213
+ blocker_ids = state.blocker_ids
1214
+ reason = state.disabled_reason
1228
1215
  if scan.unreadable_reason:
1216
+ blocker_ids = ()
1229
1217
  reason = "§1 승인 게이트를 읽을 수 없어 승인이 비활성화되었습니다 — 보고서를 재렌더하세요."
1230
1218
  elif scan.blockers:
1231
1219
  blocker_ids = tuple(b.row_id for b in scan.blockers)
1232
1220
  reason = f"§1 승인 차단 항목 {len(scan.blockers)}건 미해소"
1233
- else:
1234
- reason = ""
1235
1221
  return PlanApprovalContext(
1236
- option_names=names,
1237
- recommended_option=recommended,
1222
+ option_names=state.option_names,
1223
+ recommended_option=state.recommended_option,
1238
1224
  disabled_reason=reason,
1239
1225
  blocker_ids=blocker_ids,
1240
1226
  )
@@ -1318,22 +1304,8 @@ def _approval_label(data: dict, approval_ctx: PlanApprovalContext | None) -> str
1318
1304
 
1319
1305
 
1320
1306
  def _resolve_recommended_option(rec_name: str, names: tuple[str, ...]) -> str:
1321
- """``recommendedOption.name`` 후보 이름에 맞춘다. 정확 일치가 없으면
1322
- 접두 일치(후보가 ``(RECOMMENDED)`` 같은 표식 접미를 더 달고 있는 흔한 경우)를
1323
- 허용하되, 후보 이름이 서로의 접두여서 둘 이상이 매칭되면 모호하므로 채택하지
1324
- 않고 첫 후보로 폴백한다 — 임의의 한 후보를 권장으로 잘못 표시하느니 결정적인
1325
- 폴백이 낫다. 항상 정확히 한 옵션이 selected 가 되어 브라우저 자동선택분의 묵시
1326
- Export 를 차단한다."""
1327
- if rec_name in names:
1328
- return rec_name
1329
- if rec_name:
1330
- matches = [
1331
- name for name in names
1332
- if name.startswith(rec_name) or rec_name.startswith(name)
1333
- ]
1334
- if len(matches) == 1:
1335
- return matches[0]
1336
- return names[0]
1307
+ """Compatibility wrapper for the shared v2 approval-state resolver."""
1308
+ return resolve_recommended_option(rec_name, names)
1337
1309
 
1338
1310
 
1339
1311
  def _approval_blocked_guidance(ctx: PlanApprovalContext, run_meta: RunMeta) -> str:
@@ -53,13 +53,14 @@ from .material import (
53
53
  related_tasks_inline,
54
54
  resolve_related_tasks,
55
55
  )
56
- from .final_report_schema import load_schema
56
+ from .final_report_schema import load_schema_version
57
57
  from .final_report_paths import final_report_data_path as _final_report_data_path
58
58
  from .lead_events import LeadEvent, append_lead_event
59
59
  from .lead_runtime import ALLOWED_LEAD_RUNTIMES
60
60
  from .model_discovery import normalize_execution_for_dispatch
61
61
  from .models import ModelAssignment, default_model, resolve_model_metadata
62
62
  from .schema_excerpt import build_schema_excerpt
63
+ from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
63
64
  from .path_resolve import relative_to_project_root, resolve_user_file
64
65
  from .render import (
65
66
  apply_lead_prompt_defaults,
@@ -264,11 +265,14 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
264
265
  try:
265
266
  from .render_final_report import (
266
267
  FinalReportRenderError,
267
- find_default_template,
268
+ find_default_template_for_data,
268
269
  render,
269
270
  )
270
271
 
271
- rendered = render(data, template_path=find_default_template())
272
+ rendered = render(
273
+ data,
274
+ template_path=find_default_template_for_data(data),
275
+ )
272
276
  except FinalReportRenderError as exc:
273
277
  raise PrepareError(
274
278
  f"--approve could not re-render approved plan from data.json: {exc}"
@@ -892,7 +896,7 @@ def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _Resolv
892
896
  workspace_root / "templates" / "project-docs" / "task-index.template.md"
893
897
  )
894
898
  final_report_template = (
895
- workspace_root / "templates" / "reports" / "final-report.template.md"
899
+ workspace_root / "templates" / "reports" / "final-report-v2.template.md"
896
900
  )
897
901
  lead_contract = workspace_root / "prompts" / "lead" / "okstra-lead-contract.md"
898
902
  run_validator = workspace_root / "validators" / "validate-run.py"
@@ -1766,7 +1770,7 @@ def _render_lead_prompt_and_snapshot(
1766
1770
  str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
1767
1771
  )
1768
1772
  # Per-task-type schema excerpt for the report-writer worker. The full
1769
- # schema validates the data.json post-hoc (load_schema); the worker only
1773
+ # schema validates the data.json post-hoc; the worker only
1770
1774
  # needs the common structure + this run's task-type block, so we write a
1771
1775
  # scoped excerpt into the instruction-set rather than make the worker read
1772
1776
  # the whole 44 KB / all-task-types schema (whose repo `schemas/...` path is
@@ -1779,7 +1783,9 @@ def _render_lead_prompt_and_snapshot(
1779
1783
  # validation runs against the full schema regardless.
1780
1784
  try:
1781
1785
  _excerpt = build_schema_excerpt(
1782
- load_schema(), inp.task_type, installed_version()
1786
+ load_schema_version(CURRENT_REPORT_SCHEMA_VERSION),
1787
+ inp.task_type,
1788
+ installed_version(),
1783
1789
  )
1784
1790
  Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
1785
1791
  json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
@@ -24,21 +24,10 @@ from __future__ import annotations
24
24
  import json
25
25
  import re
26
26
 
27
- # task-type the per-type deliverable property key it owns. task-types
28
- # absent from this map (requirements-discovery,
29
- # improvement-discovery) have no per-type block; their excerpt keeps only
30
- # the non-analysis common properties.
31
- _TASK_TYPE_PROPERTY = {
32
- "error-analysis": "errorAnalysis",
33
- "project-analysis": "projectAnalysis",
34
- "feature-analysis": "featureAnalysis",
35
- "change-impact-analysis": "changeImpactAnalysis",
36
- "implementation-planning": "implementationPlanning",
37
- "release-handoff": "releaseHandoff",
38
- "implementation": "implementation",
39
- "final-verification": "finalVerification",
40
- }
41
- _ALL_PER_TYPE_PROPERTIES = frozenset(_TASK_TYPE_PROPERTY.values())
27
+ from .report_contract import TASK_TYPE_DATA_PROPERTY
28
+
29
+
30
+ _ALL_PER_TYPE_PROPERTIES = frozenset(TASK_TYPE_DATA_PROPERTY.values())
42
31
  _ANALYSIS_COMMON_TASK_TYPES = frozenset(
43
32
  {"project-analysis", "feature-analysis", "change-impact-analysis"}
44
33
  )
@@ -104,7 +93,7 @@ def build_schema_excerpt(schema: dict, task_type: str, cut_from_version: str = "
104
93
  itself in between, so a mismatch is what turns an opaque "additional
105
94
  property … not allowed" into "the bundle excerpt is from an older okstra".
106
95
  """
107
- keep_per_type = _TASK_TYPE_PROPERTY.get(task_type)
96
+ keep_per_type = TASK_TYPE_DATA_PROPERTY.get(task_type)
108
97
  drop_props = _ALL_PER_TYPE_PROPERTIES - ({keep_per_type} if keep_per_type else set())
109
98
  if task_type not in _ANALYSIS_COMMON_TASK_TYPES:
110
99
  drop_props = drop_props | {"analysisCommon"}
@@ -130,10 +119,11 @@ def build_schema_excerpt(schema: dict, task_type: str, cut_from_version: str = "
130
119
  for k, v in schema.items()
131
120
  if k not in ("properties", "allOf", "$defs", "required", "description")
132
121
  }
122
+ schema_id = schema.get("$id", "the full final-report schema")
133
123
  excerpt["description"] = (
134
124
  f"Per-task-type excerpt of the okstra final-report schema, scoped to "
135
125
  f"`{task_type}`. Reading aid for the report-writer worker — validation "
136
- f"runs against the full schema (schemas/final-report-v1.0.schema.json)."
126
+ f"runs against the full schema ({schema_id})."
137
127
  )
138
128
  excerpt["properties"] = props
139
129
  excerpt["required"] = [