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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
6
|
+
<section data-report-section="handoff-outcome">
|
|
7
|
+
<h2>인계 결과</h2>
|
|
8
|
+
{{ render_narrative(narrative.handoffOutcome, "releaseHandoff.userNarrative.handoffOutcome") }}
|
|
9
|
+
{{ figure(handoffFigure) }}
|
|
10
|
+
</section>
|
|
11
|
+
|
|
12
|
+
<section data-report-section="user-selection" data-report-field="releaseHandoff.userSelections">
|
|
13
|
+
<h2>사용자가 선택한 동작</h2>
|
|
14
|
+
<dl><dt>인계 방식</dt><dd>{{ handoff.userSelections.h1 }}</dd><dt>충돌 확인</dt><dd>{{ handoff.userSelections.h2b }}</dd><dt>PR 본문</dt><dd>{{ handoff.userSelections.h3 }}</dd></dl>
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<section data-report-section="branch-and-pr" data-report-field="releaseHandoff.featureBranchState">
|
|
18
|
+
<h2>브랜치와 PR</h2>
|
|
19
|
+
{{ render_narrative(narrative.branchAndPrExplanation, "releaseHandoff.userNarrative.branchAndPrExplanation") }}
|
|
20
|
+
<p><strong>브랜치:</strong> {{ handoff.featureBranchState.branchName }}</p>
|
|
21
|
+
<p data-report-field="releaseHandoff.pullRequestOutcome"><strong>PR 결과:</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">Pull request 열기</a>{% endif %}</p>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section data-report-section="conflicts" data-report-field="releaseHandoff.mergeConflictProbe">
|
|
25
|
+
<h2>충돌 판정</h2>
|
|
26
|
+
{{ render_narrative(narrative.conflictExplanation, "releaseHandoff.userNarrative.conflictExplanation") }}
|
|
27
|
+
{{ summary_card(handoff.mergeConflictProbe.kind, "기준 브랜치: " ~ (handoff.mergeConflictProbe.baseBranch | default('not-run'))) }}
|
|
28
|
+
{% if handoff.mergeConflictProbe.conflictingPaths %}<ul>{% for path in handoff.mergeConflictProbe.conflictingPaths %}<li>{{ path }}</li>{% endfor %}</ul>{% endif %}
|
|
29
|
+
</section>
|
|
30
|
+
|
|
31
|
+
<section data-report-section="next-action">
|
|
32
|
+
<h2>다음 행동</h2>
|
|
33
|
+
{{ render_narrative(narrative.nextAction, "releaseHandoff.userNarrative.nextAction") }}
|
|
34
|
+
<p>{{ handoff.routingRecommendation }}</p>
|
|
35
|
+
</section>
|
|
36
|
+
{% endblock %}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
6
|
+
<section data-report-section="request">
|
|
7
|
+
<h2>요청 이해</h2>
|
|
8
|
+
{{ render_narrative(narrative.requestUnderstanding, "requirementsDiscovery.userNarrative.requestUnderstanding") }}
|
|
9
|
+
<h3>사용자 요청 원문</h3>
|
|
10
|
+
<p data-report-field="requirementsDiscovery.requestVerbatim">{{ requirements.requestVerbatim }}</p>
|
|
11
|
+
<h3>시스템 해석</h3>
|
|
12
|
+
<p data-report-field="requirementsDiscovery.systemInterpretation">{{ requirements.systemInterpretation }}</p>
|
|
13
|
+
<dl data-report-field="requirementsDiscovery.classification">{% for key, value in requirements.classification.items() %}<dt>{{ key }}</dt><dd>{{ value }}</dd>{% endfor %}</dl>
|
|
14
|
+
</section>
|
|
15
|
+
|
|
16
|
+
<section data-report-section="requirements">
|
|
17
|
+
<h2>확정 요구사항과 미확정 요구사항</h2>
|
|
18
|
+
{{ render_narrative(narrative.confirmedRequirements, "requirementsDiscovery.userNarrative.confirmedRequirements") }}
|
|
19
|
+
<div class="summary-grid" data-report-field="requirementsDiscovery.confirmedRequirements">{% for row in requirements.confirmedRequirements %}{{ summary_card(row.id ~ " · 확정", row.statement) }}{% endfor %}</div>
|
|
20
|
+
{{ render_narrative(narrative.unresolvedRequirements, "requirementsDiscovery.userNarrative.unresolvedRequirements") }}
|
|
21
|
+
<div class="summary-grid" data-report-field="requirementsDiscovery.unresolvedRequirements">{% for row in requirements.unresolvedRequirements %}{{ summary_card(row.id ~ " · 결정 필요", row.statement, "important") }}{% else %}<p>미확정 요구사항이 없습니다.</p>{% endfor %}</div>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section data-report-section="routing">
|
|
25
|
+
<h2>다음 태스크와 의존성</h2>
|
|
26
|
+
{{ render_narrative(narrative.routingExplanation, "requirementsDiscovery.userNarrative.routingExplanation") }}
|
|
27
|
+
<p><strong>다음 태스크:</strong> {{ requirements.routing.nextTaskType }} · {{ requirements.routing.rationale }}</p>
|
|
28
|
+
{{ figure(routingFigure) }}
|
|
29
|
+
{% if not requirements.taskDependencies %}<p class="empty-state">이 요구사항 태스크는 다른 태스크에 의존하지 않는 단일 작업입니다.</p>{% endif %}
|
|
30
|
+
</section>
|
|
31
|
+
|
|
32
|
+
<section data-report-section="decisions">
|
|
33
|
+
<h2>사용자가 결정할 항목</h2>
|
|
34
|
+
<div class="summary-grid">{% for row in clarificationItems %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>추가 결정이 필요하지 않습니다.</p>{% endfor %}</div>
|
|
35
|
+
{{ action_list(humanSummary.actions) }}
|
|
36
|
+
</section>
|
|
37
|
+
{% endblock %}
|
|
@@ -23,6 +23,8 @@ per line to stderr.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
26
28
|
import re
|
|
27
29
|
import sys
|
|
28
30
|
from pathlib import Path
|
|
@@ -43,6 +45,8 @@ from okstra_ctl.report_views import ( # noqa: E402
|
|
|
43
45
|
source_digest,
|
|
44
46
|
)
|
|
45
47
|
from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
|
|
48
|
+
from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
|
|
49
|
+
from okstra_ctl.report_contract import TASK_TYPE_REQUIRED_HUMAN_FIELDS # noqa: E402
|
|
46
50
|
|
|
47
51
|
|
|
48
52
|
_EXTERNAL_URL_RE = re.compile(
|
|
@@ -66,11 +70,86 @@ _ANALYSIS_REVIEW_STATUSES = ["accepted", "rejected", "revision-requested"]
|
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def _main_body(html_text: str) -> str:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if start < 0 or end < 0:
|
|
73
|
+
match = re.search(r"<main[^>]*>(?P<body>.*?)</main>", html_text, re.DOTALL)
|
|
74
|
+
if match is None:
|
|
72
75
|
return html_text
|
|
73
|
-
return
|
|
76
|
+
return match.group("body")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sha256(path: Path) -> str:
|
|
80
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _digest_from_run_meta(html_text: str, key: str) -> str:
|
|
84
|
+
match = re.search(rf'"{re.escape(key)}"\s*:\s*"([0-9a-f]{{64}})"', html_text)
|
|
85
|
+
return match.group(1) if match else ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_v2_data(report_path: Path) -> tuple[Path, dict] | None:
|
|
89
|
+
data_path = final_report_data_path(report_path)
|
|
90
|
+
if not data_path.is_file():
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
94
|
+
except (OSError, json.JSONDecodeError):
|
|
95
|
+
return None
|
|
96
|
+
if isinstance(data, dict) and data.get("schemaVersion") == "2.0":
|
|
97
|
+
return data_path, data
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
|
|
102
|
+
html_path = html_view_path(report_path)
|
|
103
|
+
if not html_path.is_file():
|
|
104
|
+
return [f"missing html artifact: {html_path}"]
|
|
105
|
+
html_text = html_path.read_text(encoding="utf-8")
|
|
106
|
+
main_body = _main_body(html_text)
|
|
107
|
+
failures: list[str] = []
|
|
108
|
+
|
|
109
|
+
for key, path in (
|
|
110
|
+
("source-data-sha256", data_path),
|
|
111
|
+
("source-md-sha256", report_path),
|
|
112
|
+
):
|
|
113
|
+
recorded = _digest_from_run_meta(html_text, key)
|
|
114
|
+
expected = _sha256(path)
|
|
115
|
+
if not recorded:
|
|
116
|
+
failures.append(f"html missing {key} in run-meta")
|
|
117
|
+
elif recorded != expected:
|
|
118
|
+
failures.append(f"stale html: {key} does not match current source")
|
|
119
|
+
|
|
120
|
+
task_type = data.get("header", {}).get("taskType", "")
|
|
121
|
+
if f'data-task-template="{task_type}"' not in html_text:
|
|
122
|
+
failures.append(f"v2 html task template mismatch for {task_type}")
|
|
123
|
+
actual_fields = set(re.findall(r'data-report-field="([^"]+)"', main_body))
|
|
124
|
+
for field in TASK_TYPE_REQUIRED_HUMAN_FIELDS.get(task_type, ()):
|
|
125
|
+
if field not in actual_fields:
|
|
126
|
+
failures.append(f"missing human field: {field}")
|
|
127
|
+
|
|
128
|
+
if "data-audit-field=" in main_body:
|
|
129
|
+
failures.append("audit field inside human main")
|
|
130
|
+
node_ids = set(re.findall(r'data-node-id="([^"]+)"', html_text))
|
|
131
|
+
fallback_ids = set(re.findall(r'data-fallback-id="([^"]+)"', html_text))
|
|
132
|
+
if node_ids != fallback_ids:
|
|
133
|
+
failures.append(
|
|
134
|
+
f"visualization ID mismatch: svg={sorted(node_ids)}, fallback={sorted(fallback_ids)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
data_ids = []
|
|
138
|
+
for row in data.get("clarificationItems", []):
|
|
139
|
+
if not isinstance(row, dict):
|
|
140
|
+
continue
|
|
141
|
+
response_id = row.get("id")
|
|
142
|
+
if isinstance(response_id, str) and re.fullmatch(r"C-\d+", response_id):
|
|
143
|
+
data_ids.append(response_id)
|
|
144
|
+
data_ids.sort()
|
|
145
|
+
html_ids = sorted(set(_RESPONSE_ID_ATTR_RE.findall(html_text)))
|
|
146
|
+
if data_ids != html_ids:
|
|
147
|
+
failures.append(
|
|
148
|
+
f"Response ID mismatch: data.json has {data_ids}, HTML has {html_ids}"
|
|
149
|
+
)
|
|
150
|
+
if _EXTERNAL_URL_RE.search(html_text):
|
|
151
|
+
failures.append("html contains external URL in script/link/img — must be self-contained")
|
|
152
|
+
return failures
|
|
74
153
|
|
|
75
154
|
|
|
76
155
|
def _no_form_sections(html_body: str) -> list[str]:
|
|
@@ -128,6 +207,9 @@ def validate(report_path: Path) -> list[str]:
|
|
|
128
207
|
return [f"final-report not found: {report_path}"]
|
|
129
208
|
|
|
130
209
|
md = report_path.read_text(encoding="utf-8")
|
|
210
|
+
v2 = _load_v2_data(report_path)
|
|
211
|
+
if v2 is not None:
|
|
212
|
+
return _validate_v2(report_path, v2[0], v2[1])
|
|
131
213
|
html_path = html_view_path(report_path)
|
|
132
214
|
# §1 헤딩이 있는데 파싱이 실패하면 md_ids 가 빈 []이 되어 "clarification 없음
|
|
133
215
|
# → skip" 으로 흘러 HTML form parity 게이트가 조용히 열린다. fail-closed.
|
|
@@ -30,12 +30,12 @@ for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "
|
|
|
30
30
|
try:
|
|
31
31
|
from okstra_ctl.final_report_schema import (
|
|
32
32
|
SchemaError,
|
|
33
|
-
|
|
33
|
+
load_schema_for_data,
|
|
34
34
|
validate as schema_validate,
|
|
35
35
|
)
|
|
36
36
|
except ImportError: # pragma: no cover — runtime guarantees this import
|
|
37
37
|
SchemaError = None # type: ignore[assignment]
|
|
38
|
-
|
|
38
|
+
load_schema_for_data = None # type: ignore[assignment]
|
|
39
39
|
schema_validate = None # type: ignore[assignment]
|
|
40
40
|
|
|
41
41
|
from okstra_project import project_json_path # noqa: E402
|
|
@@ -1021,6 +1021,48 @@ def _scan_token_usage_summary(
|
|
|
1021
1021
|
# a section heading line (not as inline text inside a paragraph or table).
|
|
1022
1022
|
_VERDICT_CARD_HEADING_RE = re.compile(r"^##[ \t]+Verdict Card\b", re.MULTILINE)
|
|
1023
1023
|
|
|
1024
|
+
_V2_AI_HANDOFF_HEADINGS = (
|
|
1025
|
+
"## AI Handoff Summary",
|
|
1026
|
+
"## Clarification and User Decisions",
|
|
1027
|
+
"## Evidence Ledger",
|
|
1028
|
+
"## Task Deliverable:",
|
|
1029
|
+
"## Cross Verification Audit",
|
|
1030
|
+
"## Execution Audit",
|
|
1031
|
+
"## Token and Cost Audit",
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _validate_v2_ai_handoff(content: str, failures: list[str]) -> None:
|
|
1036
|
+
positions: list[int] = []
|
|
1037
|
+
for heading in _V2_AI_HANDOFF_HEADINGS:
|
|
1038
|
+
count = content.count(heading)
|
|
1039
|
+
if count != 1:
|
|
1040
|
+
failures.append(
|
|
1041
|
+
"schema-v2 AI handoff markdown requires exactly one "
|
|
1042
|
+
f"`{heading}` heading; found {count}."
|
|
1043
|
+
)
|
|
1044
|
+
continue
|
|
1045
|
+
positions.append(content.index(heading))
|
|
1046
|
+
if len(positions) == len(_V2_AI_HANDOFF_HEADINGS) and positions != sorted(
|
|
1047
|
+
positions
|
|
1048
|
+
):
|
|
1049
|
+
failures.append(
|
|
1050
|
+
"schema-v2 AI handoff markdown heading order does not match "
|
|
1051
|
+
"templates/reports/final-report-v2.template.md."
|
|
1052
|
+
)
|
|
1053
|
+
for human_only_field in ('"humanSummary"', '"userNarrative"'):
|
|
1054
|
+
if human_only_field in content:
|
|
1055
|
+
failures.append(
|
|
1056
|
+
"schema-v2 AI handoff markdown contains human-only field "
|
|
1057
|
+
f"{human_only_field}; render it only in the task-specific HTML."
|
|
1058
|
+
)
|
|
1059
|
+
if _READING_CONFIRMATION_HEADING_RE.search(content) is not None:
|
|
1060
|
+
failures.append(
|
|
1061
|
+
"final report contains a `## 0. Reading Confirmation` heading — "
|
|
1062
|
+
"Reading Confirmation lives in the worker audit sidecar, never "
|
|
1063
|
+
"in the AI handoff markdown."
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1024
1066
|
# Top-of-report Index block. The renderer
|
|
1025
1067
|
# (scripts/okstra_ctl/render_final_report.py) injects `<a id="report-index">`
|
|
1026
1068
|
# into the index heading; a missing anchor means the markdown was produced
|
|
@@ -2229,6 +2271,10 @@ def validate_report(
|
|
|
2229
2271
|
"run `okstra-token-usage.py ... --substitute-data <report-path>` during Phase 7"
|
|
2230
2272
|
)
|
|
2231
2273
|
|
|
2274
|
+
if (report_data or {}).get("schemaVersion") == "2.0":
|
|
2275
|
+
_validate_v2_ai_handoff(content, failures)
|
|
2276
|
+
return
|
|
2277
|
+
|
|
2232
2278
|
# Catch the "workers typed `0` / `pending` instead of the placeholder"
|
|
2233
2279
|
# failure mode that bypasses the placeholder check above.
|
|
2234
2280
|
_scan_token_usage_summary(
|
|
@@ -2995,7 +3041,7 @@ def validate_final_report_data(
|
|
|
2995
3041
|
project_root: Path | None = None,
|
|
2996
3042
|
clarification_text: str = "",
|
|
2997
3043
|
) -> Mapping[str, Any] | None:
|
|
2998
|
-
"""Validate
|
|
3044
|
+
"""Validate final-report data.json against its declared versioned schema.
|
|
2999
3045
|
|
|
3000
3046
|
The data.json is the source-of-truth that the renderer reads to
|
|
3001
3047
|
produce the markdown. If schema validation passes here, the rendered
|
|
@@ -3010,7 +3056,7 @@ def validate_final_report_data(
|
|
|
3010
3056
|
The returned mapping is the exact loaded snapshot consumed by later
|
|
3011
3057
|
finalization checks and workflow persistence.
|
|
3012
3058
|
"""
|
|
3013
|
-
if schema_validate is None or
|
|
3059
|
+
if schema_validate is None or load_schema_for_data is None:
|
|
3014
3060
|
# Module-load fallback path; should never fire in a real install.
|
|
3015
3061
|
failures.append(
|
|
3016
3062
|
"validate-run: okstra_ctl.final_report_schema is not importable — "
|
|
@@ -3028,13 +3074,11 @@ def validate_final_report_data(
|
|
|
3028
3074
|
return
|
|
3029
3075
|
|
|
3030
3076
|
try:
|
|
3031
|
-
|
|
3077
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
3078
|
+
schema = load_schema_for_data(data)
|
|
3032
3079
|
except SchemaError as exc:
|
|
3033
3080
|
failures.append(f"final-report schema could not be loaded: {exc}")
|
|
3034
3081
|
return
|
|
3035
|
-
|
|
3036
|
-
try:
|
|
3037
|
-
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
3038
3082
|
except json.JSONDecodeError as exc:
|
|
3039
3083
|
failures.append(f"final-report data.json is not valid JSON: {exc}")
|
|
3040
3084
|
return
|
|
@@ -6035,6 +6079,8 @@ def validate_phase_boundary(
|
|
|
6035
6079
|
task_type: str,
|
|
6036
6080
|
report_path: Path,
|
|
6037
6081
|
failures: list[str],
|
|
6082
|
+
*,
|
|
6083
|
+
report_data: Mapping[str, Any] | None = None,
|
|
6038
6084
|
) -> None:
|
|
6039
6085
|
"""Phase-specific contract checks.
|
|
6040
6086
|
|
|
@@ -6050,6 +6096,8 @@ def validate_phase_boundary(
|
|
|
6050
6096
|
MUST be absent (lead converted findings into Clarification rows
|
|
6051
6097
|
instead of opening the gate).
|
|
6052
6098
|
"""
|
|
6099
|
+
if (report_data or {}).get("schemaVersion") == "2.0":
|
|
6100
|
+
return
|
|
6053
6101
|
if not report_path.exists():
|
|
6054
6102
|
return
|
|
6055
6103
|
content = report_path.read_text()
|
|
@@ -7167,7 +7215,12 @@ def main() -> int:
|
|
|
7167
7215
|
)
|
|
7168
7216
|
validate_team_state_usage(team_state, failures)
|
|
7169
7217
|
|
|
7170
|
-
validate_phase_boundary(
|
|
7218
|
+
validate_phase_boundary(
|
|
7219
|
+
task_type,
|
|
7220
|
+
report_path,
|
|
7221
|
+
failures,
|
|
7222
|
+
report_data=validation_data,
|
|
7223
|
+
)
|
|
7171
7224
|
_validate_phase_boundary_error_log(
|
|
7172
7225
|
report_path.parent.parent,
|
|
7173
7226
|
task_type,
|
|
@@ -7,6 +7,7 @@ Called by validators/validate-run.py when task_type == "improvement-discovery".
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import json
|
|
10
11
|
import re
|
|
11
12
|
import sys
|
|
12
13
|
from dataclasses import dataclass, field
|
|
@@ -25,6 +26,10 @@ from okstra_ctl.improvement_lenses import (
|
|
|
25
26
|
ABSOLUTE_CANDIDATE_CAP,
|
|
26
27
|
SOURCE_WORKERS,
|
|
27
28
|
)
|
|
29
|
+
from okstra_ctl.final_report_schema import (
|
|
30
|
+
load_schema_version,
|
|
31
|
+
validate as validate_final_report_data,
|
|
32
|
+
)
|
|
28
33
|
from okstra_ctl.md_table import split_pipe_row
|
|
29
34
|
|
|
30
35
|
|
|
@@ -290,6 +295,47 @@ def _check_verdict_card(
|
|
|
290
295
|
errors.append("Verdict Card row must byte-match `## 7. Final Verdict` row")
|
|
291
296
|
|
|
292
297
|
|
|
298
|
+
def _v2_candidate_rows(data: dict) -> list[list[str]]:
|
|
299
|
+
candidates = data["improvementDiscovery"]["candidates"]
|
|
300
|
+
return [
|
|
301
|
+
[
|
|
302
|
+
row["id"],
|
|
303
|
+
",".join(row["lens"]),
|
|
304
|
+
row["title"],
|
|
305
|
+
",".join(row["scope"]),
|
|
306
|
+
row["severity"],
|
|
307
|
+
row["effort"],
|
|
308
|
+
row["consensus"],
|
|
309
|
+
",".join(row["sourceWorkers"]),
|
|
310
|
+
row["recommendedNextPhase"],
|
|
311
|
+
row["expectedBehaviorAfter"],
|
|
312
|
+
",".join(row["evidence"]),
|
|
313
|
+
]
|
|
314
|
+
for row in candidates
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _validate_v2_report(
|
|
319
|
+
data: dict,
|
|
320
|
+
run_dir: Path,
|
|
321
|
+
brief_frontmatter: dict,
|
|
322
|
+
) -> ValidationResult:
|
|
323
|
+
errors = validate_final_report_data(data, load_schema_version("2.0"))
|
|
324
|
+
_check_grilling_log(run_dir, errors)
|
|
325
|
+
if errors:
|
|
326
|
+
return ValidationResult(ok=False, errors=errors)
|
|
327
|
+
|
|
328
|
+
rows = _v2_candidate_rows(data)
|
|
329
|
+
_check_candidates_cap(len(rows), brief_frontmatter, errors)
|
|
330
|
+
_check_candidate_rows(
|
|
331
|
+
rows,
|
|
332
|
+
brief_frontmatter.get("scan-scope") or [],
|
|
333
|
+
brief_frontmatter.get("out-of-scope") or [],
|
|
334
|
+
errors,
|
|
335
|
+
)
|
|
336
|
+
return ValidationResult(ok=not errors, errors=errors)
|
|
337
|
+
|
|
338
|
+
|
|
293
339
|
def validate_improvement_report(
|
|
294
340
|
report_path: Path, run_dir: Path, brief_frontmatter: dict
|
|
295
341
|
) -> ValidationResult:
|
|
@@ -299,6 +345,15 @@ def validate_improvement_report(
|
|
|
299
345
|
errors.append(f"report file not found: {report_path}")
|
|
300
346
|
return ValidationResult(ok=False, errors=errors)
|
|
301
347
|
|
|
348
|
+
data_path = report_path.with_suffix(".data.json")
|
|
349
|
+
if data_path.exists():
|
|
350
|
+
try:
|
|
351
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
352
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
353
|
+
return ValidationResult(ok=False, errors=[f"invalid report data: {exc}"])
|
|
354
|
+
if data.get("schemaVersion") == "2.0":
|
|
355
|
+
return _validate_v2_report(data, run_dir, brief_frontmatter)
|
|
356
|
+
|
|
302
357
|
body = report_path.read_text(encoding="utf-8")
|
|
303
358
|
|
|
304
359
|
_check_grilling_log(run_dir, errors)
|
|
@@ -80,7 +80,8 @@ Effect (copy mode):
|
|
|
80
80
|
${"$HOME"}/.okstra/lib/python <- runtime/python
|
|
81
81
|
${"$HOME"}/.okstra/lib/validators <- runtime/validators (stage/report structure validators)
|
|
82
82
|
${"$HOME"}/.okstra/bin <- runtime/bin
|
|
83
|
-
${"$HOME"}/.okstra/templates <- runtime/templates (
|
|
83
|
+
${"$HOME"}/.okstra/templates <- runtime/templates (v1 assets + v2 AI/HTML template trees)
|
|
84
|
+
${"$HOME"}/.okstra/schemas <- runtime/schemas (final-report schema v1 + v2)
|
|
84
85
|
${"$HOME"}/.okstra/prompts <- runtime/prompts (lead contracts + coding-preflight pack)
|
|
85
86
|
${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
|
|
86
87
|
${"$HOME"}/.claude/skills/<name> <- skills when ${"$HOME"}/.claude exists
|
|
@@ -775,8 +776,9 @@ export async function runInstall(args) {
|
|
|
775
776
|
paths.bin,
|
|
776
777
|
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 },
|
|
777
778
|
);
|
|
778
|
-
// templates/ tree — report.css
|
|
779
|
-
//
|
|
779
|
+
// templates/ tree — the v1 report.css/report.js compatibility assets, the
|
|
780
|
+
// v2 AI handoff template, and the task-specific html/** tree are consumed by
|
|
781
|
+
// okstra-render-report-views.py and final-report assembly. They
|
|
780
782
|
// are NOT covered by installNamedTemplate (which only handles the renamed
|
|
781
783
|
// settings.local.json sidecar), so without this step copy-mode installs
|
|
782
784
|
// miss every asset other than that single file. See
|
|
@@ -786,13 +788,10 @@ export async function runInstall(args) {
|
|
|
786
788
|
join(paths.home, "templates"),
|
|
787
789
|
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
|
|
788
790
|
);
|
|
789
|
-
// schemas/ tree — final-report
|
|
790
|
-
// okstra_ctl.final_report_schema.
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
// each run's instruction-set, and (b) validate-run's data.json schema
|
|
794
|
-
// check. Without this step copy-mode installs leave load_schema() unable
|
|
795
|
-
// to locate the schema. Link mode resolves it through the repo symlink.
|
|
791
|
+
// schemas/ tree — version-selected final-report v1/v2 schemas are loaded by
|
|
792
|
+
// okstra_ctl.final_report_schema. They support bundle excerpts, legacy
|
|
793
|
+
// re-renders, new task HTML, and validate-run. Link mode resolves the same
|
|
794
|
+
// files through the repo symlink.
|
|
796
795
|
const schemasResult = await copyTreeIfChanged(
|
|
797
796
|
join(runtimeRoot, "schemas"),
|
|
798
797
|
join(paths.home, "schemas"),
|
|
@@ -840,7 +839,7 @@ export async function runInstall(args) {
|
|
|
840
839
|
}
|
|
841
840
|
if (templatesResult.missingSource) {
|
|
842
841
|
process.stderr.write(
|
|
843
|
-
"warning: runtime/templates is empty. report
|
|
842
|
+
"warning: runtime/templates is empty. final-report Markdown and HTML templates will be missing — re-run the build step.\n",
|
|
844
843
|
);
|
|
845
844
|
}
|
|
846
845
|
if (schemasResult.missingSource) {
|
|
@@ -994,8 +993,14 @@ export async function runEnsureInstalled(args) {
|
|
|
994
993
|
}
|
|
995
994
|
if (!(await dirExists(paths.pythonpath))) reasons.push(`missing ${paths.pythonpath}`);
|
|
996
995
|
if (!(await dirExists(paths.agents))) reasons.push(`missing agents dir ${paths.agents}`);
|
|
997
|
-
|
|
998
|
-
|
|
996
|
+
for (const relative of [
|
|
997
|
+
["schemas", "final-report-v1.0.schema.json"],
|
|
998
|
+
["schemas", "final-report-v2.0.schema.json"],
|
|
999
|
+
["templates", "reports", "final-report-v2.template.md"],
|
|
1000
|
+
["templates", "reports", "html", "base.template.html"],
|
|
1001
|
+
]) {
|
|
1002
|
+
const installedPath = join(paths.home, ...relative);
|
|
1003
|
+
if (!(await fileExists(installedPath))) reasons.push(`missing ${installedPath}`);
|
|
999
1004
|
}
|
|
1000
1005
|
for (const reason of await promptResourceDriftReasons(paths)) {
|
|
1001
1006
|
reasons.push(reason);
|
|
@@ -10,9 +10,8 @@ Usage:
|
|
|
10
10
|
Runs the four Phase 7 steps in their contractual order against one final-report:
|
|
11
11
|
|
|
12
12
|
1. token-usage substitute real token/cost numbers into the data.json
|
|
13
|
-
2. render-views write the
|
|
14
|
-
|
|
15
|
-
approval target)
|
|
13
|
+
2. render-views write the schema-v2 task-specific *.html sibling; v1
|
|
14
|
+
keeps the legacy conditional interactive view
|
|
16
15
|
3. spawn-followups turn section 4 rows into task stubs
|
|
17
16
|
4. validate-run validate the finished run artifacts
|
|
18
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runInstalledScript } from "../../lib/python-helper.mjs";
|
|
2
2
|
|
|
3
|
-
const USAGE = `okstra render-final-report — render the
|
|
3
|
+
const USAGE = `okstra render-final-report — render the AI handoff Markdown sibling of final-report data.json
|
|
4
4
|
|
|
5
5
|
Wraps the python helper (\`okstra-render-final-report.py\`) installed under
|
|
6
6
|
\`~/.okstra/bin/\` so skills call \`okstra render-final-report\` instead of
|
|
@@ -10,7 +10,9 @@ permission matching and prompts on every call).
|
|
|
10
10
|
Usage:
|
|
11
11
|
okstra render-final-report <path-to-final-report.data.json>
|
|
12
12
|
|
|
13
|
-
The argument is forwarded verbatim to the
|
|
13
|
+
The argument is forwarded verbatim to the Python helper. schemaVersion 1.0
|
|
14
|
+
keeps the legacy Markdown template; schemaVersion 2.0 selects the AI handoff
|
|
15
|
+
template. Human HTML is rendered separately from the same data.json.
|
|
14
16
|
`;
|
|
15
17
|
|
|
16
18
|
export async function run(args) {
|
|
@@ -3,20 +3,20 @@ import { runInstalledScript } from "../../lib/python-helper.mjs";
|
|
|
3
3
|
const USAGE = `okstra render-views — render the self-contained HTML view of a final-report
|
|
4
4
|
|
|
5
5
|
A thin spawn shim over \`scripts/okstra-render-report-views.py\` (installed
|
|
6
|
-
at \`$HOME/.okstra/bin/okstra-render-report-views.py\`).
|
|
7
|
-
report
|
|
6
|
+
at \`$HOME/.okstra/bin/okstra-render-report-views.py\`). Resolves the final-
|
|
7
|
+
report data and Markdown pair, then writes a single sibling:
|
|
8
8
|
|
|
9
|
-
<stem>.html — single-file self-contained human view
|
|
10
|
-
|
|
11
|
-
report has no §5 C-* clarification rows)
|
|
9
|
+
<stem>.html — single-file self-contained human view; schema v2 is
|
|
10
|
+
always generated, while v1/quick remains conditional
|
|
12
11
|
|
|
13
12
|
Usage:
|
|
14
|
-
okstra render-views <path-to-final-report.md>
|
|
13
|
+
okstra render-views <path-to-final-report.data.json|path-to-final-report.md>
|
|
15
14
|
[--task-key <key>] [--task-type <type>] [--seq <NNN>]
|
|
16
15
|
[--source-report <project-relative-path>]
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Schema v2 data is rendered directly into its task-specific HTML view and is
|
|
18
|
+
always generated. Schema v1 and quick reports keep the legacy conditional
|
|
19
|
+
Markdown view. Optional flags override inferred report metadata.
|
|
20
20
|
`;
|
|
21
21
|
|
|
22
22
|
export async function run(args) {
|