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.
- package/README.md +2 -2
- package/docs/architecture/storage-model.md +8 -7
- package/docs/architecture.md +18 -12
- package/docs/cli.md +3 -3
- package/docs/project-structure-overview.md +16 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +10 -10
- package/runtime/bin/okstra-render-final-report.py +4 -4
- package/runtime/bin/okstra-render-report-views.py +100 -12
- package/runtime/bin/okstra-trace-cleanup.sh +13 -9
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
- package/runtime/prompts/lead/report-writer.md +15 -11
- package/runtime/prompts/profiles/_common-contract.md +13 -7
- package/runtime/prompts/profiles/improvement-discovery.md +3 -1
- package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
- package/runtime/python/okstra_ctl/render_final_report.py +136 -28
- package/runtime/python/okstra_ctl/report_contract.py +124 -0
- package/runtime/python/okstra_ctl/report_finalize.py +1 -1
- package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
- package/runtime/python/okstra_ctl/report_html/common.py +50 -0
- package/runtime/python/okstra_ctl/report_html/models.py +59 -0
- package/runtime/python/okstra_ctl/report_html/render.py +69 -0
- package/runtime/python/okstra_ctl/report_html/router.py +40 -0
- package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
- package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +38 -0
- package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +48 -0
- package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +38 -0
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +46 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +46 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +102 -0
- package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +42 -0
- package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +53 -0
- package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +54 -0
- package/runtime/python/okstra_ctl/report_html/visualizations.py +113 -0
- package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
- package/runtime/python/okstra_ctl/report_views.py +15 -43
- package/runtime/python/okstra_ctl/run.py +12 -6
- package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
- package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
- package/runtime/templates/reports/final-report-v2.template.md +66 -0
- package/runtime/templates/reports/html/assets/base.css +38 -0
- package/runtime/templates/reports/html/assets/base.js +5 -0
- package/runtime/templates/reports/html/base.template.html +65 -0
- package/runtime/templates/reports/html/macros/forms.html +47 -0
- package/runtime/templates/reports/html/macros/layout.html +19 -0
- package/runtime/templates/reports/html/macros/visualizations.html +17 -0
- package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
- package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
- package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
- package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
- package/runtime/validators/validate-report-views.py +86 -4
- package/runtime/validators/validate-run.py +62 -9
- package/runtime/validators/validate_improvement_report.py +55 -0
- package/src/commands/lifecycle/install.mjs +18 -13
- package/src/commands/report/finalize.mjs +2 -3
- package/src/commands/report/render-final-report.mjs +4 -2
- 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
|
-
|
|
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
|
-
|
|
1214
|
-
if
|
|
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
|
|
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=
|
|
1237
|
-
recommended_option=
|
|
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
|
-
"""
|
|
1322
|
-
|
|
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
|
|
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
|
-
|
|
268
|
+
find_default_template_for_data,
|
|
268
269
|
render,
|
|
269
270
|
)
|
|
270
271
|
|
|
271
|
-
rendered = render(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 =
|
|
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 (
|
|
126
|
+
f"runs against the full schema ({schema_id})."
|
|
137
127
|
)
|
|
138
128
|
excerpt["properties"] = props
|
|
139
129
|
excerpt["required"] = [
|