okstra 0.149.0 → 0.151.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/docs/architecture.md +1 -1
- package/docs/cli.md +2 -2
- package/docs/project-structure-overview.md +1 -1
- package/package.json +3 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +8 -0
- package/runtime/agents/workers/translator-worker.md +67 -0
- package/runtime/bin/okstra-render-final-report.py +0 -11
- package/runtime/bin/okstra-report-translate.py +191 -0
- package/runtime/prompts/lead/adapters/claude-code.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
- package/runtime/prompts/lead/plan-body-verification.md +1 -1
- package/runtime/prompts/lead/report-writer.md +16 -15
- package/runtime/prompts/lead/team-contract.md +2 -2
- package/runtime/prompts/wizard/prompts.ko.json +17 -1
- package/runtime/python/okstra_ctl/analysis_inputs.py +24 -9
- package/runtime/python/okstra_ctl/analysis_packet.py +23 -1
- package/runtime/python/okstra_ctl/clarification_items.py +241 -44
- package/runtime/python/okstra_ctl/codex_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/convergence.py +15 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +2 -2
- package/runtime/python/okstra_ctl/dispatch_state.py +12 -1
- package/runtime/python/okstra_ctl/final_report_paths.py +22 -1
- package/runtime/python/okstra_ctl/i18n.py +12 -7
- package/runtime/python/okstra_ctl/render_final_report.py +18 -17
- package/runtime/python/okstra_ctl/report_finalize.py +18 -0
- package/runtime/python/okstra_ctl/report_html/filters.py +15 -77
- package/runtime/python/okstra_ctl/report_html/render.py +44 -2
- package/runtime/python/okstra_ctl/report_translation.py +469 -0
- package/runtime/python/okstra_ctl/report_views.py +23 -9
- package/runtime/python/okstra_ctl/run.py +1 -1
- package/runtime/python/okstra_ctl/user_response.py +11 -6
- package/runtime/python/okstra_ctl/wizard.py +100 -25
- package/runtime/python/okstra_ctl/worker_liveness.py +130 -36
- package/runtime/templates/reports/html/base.template.html +12 -12
- package/runtime/templates/reports/html/i18n/en.json +395 -0
- package/runtime/templates/reports/html/i18n/ko.json +395 -0
- package/runtime/templates/reports/html/macros/forms.html +16 -16
- package/runtime/templates/reports/html/macros/visualizations.html +2 -2
- package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +17 -17
- package/runtime/templates/reports/html/tasks/error-analysis.template.html +12 -12
- package/runtime/templates/reports/html/tasks/feature-analysis.template.html +16 -16
- package/runtime/templates/reports/html/tasks/final-verification.template.html +12 -12
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +37 -37
- package/runtime/templates/reports/html/tasks/implementation.template.html +18 -18
- package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +7 -7
- package/runtime/templates/reports/html/tasks/project-analysis.template.html +29 -29
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +13 -13
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +14 -14
- package/runtime/templates/reports/report.js +8 -5
- package/runtime/validators/validate-report-views.py +1 -1
- package/runtime/validators/validate-run.py +59 -31
- package/src/cli-registry.mjs +11 -0
- package/src/commands/inspect/worker-liveness.mjs +9 -7
- package/src/commands/report/translate.mjs +31 -0
- package/src/lib/helper-scripts.mjs +1 -0
- package/runtime/templates/reports/i18n/ko.json +0 -273
|
@@ -454,7 +454,7 @@ def _inject_index_and_anchors(markdown: str, dictionary: dict | None) -> str:
|
|
|
454
454
|
return "\n".join(lines)
|
|
455
455
|
|
|
456
456
|
|
|
457
|
-
def inject_index_into_file(md_path: Path
|
|
457
|
+
def inject_index_into_file(md_path: Path) -> int:
|
|
458
458
|
"""Apply the legacy top-of-report index to an existing Markdown report.
|
|
459
459
|
|
|
460
460
|
This remains for schema-v1 and quick compatibility artifacts. Schema-v2
|
|
@@ -463,12 +463,8 @@ def inject_index_into_file(md_path: Path, *, report_language: str = "en") -> int
|
|
|
463
463
|
"""
|
|
464
464
|
if not md_path.is_file():
|
|
465
465
|
raise FinalReportRenderError(f"report markdown not found: {md_path}")
|
|
466
|
-
if report_language not in SUPPORTED_LANGS:
|
|
467
|
-
raise FinalReportRenderError(
|
|
468
|
-
f"report_language must be one of {SUPPORTED_LANGS}, got {report_language!r}"
|
|
469
|
-
)
|
|
470
466
|
try:
|
|
471
|
-
dictionary = load_dictionary(
|
|
467
|
+
dictionary = load_dictionary(MARKDOWN_LANG)
|
|
472
468
|
except I18nError as exc:
|
|
473
469
|
raise FinalReportRenderError(str(exc)) from exc
|
|
474
470
|
injected = _inject_index_and_anchors(md_path.read_text(encoding="utf-8"), dictionary)
|
|
@@ -630,12 +626,20 @@ def _ai_markdown_context(data: dict) -> dict:
|
|
|
630
626
|
return context
|
|
631
627
|
|
|
632
628
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
629
|
+
# The Markdown is the AI handoff sibling of the data.json — same content, same
|
|
630
|
+
# audience, so it renders in the SSOT's language and nothing else. The field
|
|
631
|
+
# still has to be well-formed here because it decides whether Phase 7 pays for
|
|
632
|
+
# a translator, and a malformed one would surface at the very end of the run.
|
|
633
|
+
MARKDOWN_LANG = "en"
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def validate_report_language(data: dict) -> str:
|
|
637
|
+
"""Check `meta.reportLanguage` and return the Markdown's own language.
|
|
638
|
+
|
|
639
|
+
The value names the language the *human HTML* renders in; it never
|
|
640
|
+
selects this renderer's dictionary.
|
|
641
|
+
"""
|
|
642
|
+
candidate = (data.get("meta") or {}).get("reportLanguage") or "en"
|
|
639
643
|
if candidate == "auto":
|
|
640
644
|
raise FinalReportRenderError(
|
|
641
645
|
"reportLanguage 'auto' must be resolved by the lead before "
|
|
@@ -646,7 +650,7 @@ def resolve_report_language(data: dict, *, override: str | None) -> str:
|
|
|
646
650
|
raise FinalReportRenderError(
|
|
647
651
|
f"reportLanguage must be one of {SUPPORTED_LANGS}, got {candidate!r}"
|
|
648
652
|
)
|
|
649
|
-
return
|
|
653
|
+
return MARKDOWN_LANG
|
|
650
654
|
|
|
651
655
|
|
|
652
656
|
# Schema-optional top-level arrays, and the value a template may assume when
|
|
@@ -685,7 +689,6 @@ def render(
|
|
|
685
689
|
data: dict,
|
|
686
690
|
*,
|
|
687
691
|
template_path: Path,
|
|
688
|
-
report_language: str | None = None,
|
|
689
692
|
) -> str:
|
|
690
693
|
"""Render ``data`` through the Jinja2 ``template_path`` and return the
|
|
691
694
|
final markdown as a string. Caller writes it to disk.
|
|
@@ -699,7 +702,7 @@ def render(
|
|
|
699
702
|
|
|
700
703
|
_enforce_schema(data)
|
|
701
704
|
|
|
702
|
-
lang =
|
|
705
|
+
lang = validate_report_language(data)
|
|
703
706
|
try:
|
|
704
707
|
dictionary = load_dictionary(lang)
|
|
705
708
|
except I18nError as exc:
|
|
@@ -834,7 +837,6 @@ def render_to_file(
|
|
|
834
837
|
output_path: Path,
|
|
835
838
|
*,
|
|
836
839
|
template_path: Path | None = None,
|
|
837
|
-
report_language: str | None = None,
|
|
838
840
|
) -> int:
|
|
839
841
|
"""Read ``data_path`` (JSON), render through Jinja2, write to
|
|
840
842
|
``output_path``. Returns the number of bytes written.
|
|
@@ -859,7 +861,6 @@ def render_to_file(
|
|
|
859
861
|
rendered = render(
|
|
860
862
|
data,
|
|
861
863
|
template_path=resolved_template,
|
|
862
|
-
report_language=report_language,
|
|
863
864
|
)
|
|
864
865
|
except FinalReportRenderError as exc:
|
|
865
866
|
raise _with_excerpt_drift_hint(exc, data_path) from exc
|
|
@@ -24,12 +24,17 @@ from .final_report_paths import final_report_data_path, final_report_markdown_pa
|
|
|
24
24
|
from .paths import task_dir, task_manifest_file
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
STEP_CHECK_SOURCE = "check-source"
|
|
27
28
|
STEP_TOKEN_USAGE = "token-usage"
|
|
28
29
|
STEP_RENDER_VIEWS = "render-views"
|
|
29
30
|
STEP_SPAWN_FOLLOWUPS = "spawn-followups"
|
|
30
31
|
STEP_VALIDATE_RUN = "validate-run"
|
|
31
32
|
|
|
32
33
|
STEP_ORDER = (
|
|
34
|
+
# First, because everything after it derives from the data.json: rendering
|
|
35
|
+
# a Korean SSOT into English chrome, spawning follow-ups from it, and
|
|
36
|
+
# validating it all succeed on a record the next phase cannot read.
|
|
37
|
+
STEP_CHECK_SOURCE,
|
|
33
38
|
STEP_TOKEN_USAGE,
|
|
34
39
|
STEP_RENDER_VIEWS,
|
|
35
40
|
STEP_SPAWN_FOLLOWUPS,
|
|
@@ -205,6 +210,19 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
|
205
210
|
"""Assemble the ordered Phase 7 argv list. Order is contractual."""
|
|
206
211
|
markdown_path = ctx.markdown_path
|
|
207
212
|
return [
|
|
213
|
+
(
|
|
214
|
+
STEP_CHECK_SOURCE,
|
|
215
|
+
[
|
|
216
|
+
sys.executable,
|
|
217
|
+
str(
|
|
218
|
+
resolve_workspace_script(
|
|
219
|
+
ctx.workspace_root, "okstra-report-translate.py"
|
|
220
|
+
)
|
|
221
|
+
),
|
|
222
|
+
"check-source",
|
|
223
|
+
str(ctx.data_path),
|
|
224
|
+
],
|
|
225
|
+
),
|
|
208
226
|
(
|
|
209
227
|
STEP_TOKEN_USAGE,
|
|
210
228
|
[
|
|
@@ -97,90 +97,28 @@ def evidence_refs(refs: object, anchors: object) -> Markup:
|
|
|
97
97
|
|
|
98
98
|
# Schema enums a reader has to decode. Each vocabulary is named so a value can
|
|
99
99
|
# mean different things in different fields without one table flattening them.
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"gap": "No test",
|
|
107
|
-
"risk": "Passes without proving",
|
|
108
|
-
},
|
|
109
|
-
"effort": {"S": "Small", "M": "Medium", "L": "Large", "XL": "Very large"},
|
|
110
|
-
"versionSource": {
|
|
111
|
-
"manifest-range": "Manifest range",
|
|
112
|
-
"lockfile-pinned": "Lockfile pin",
|
|
113
|
-
"container-image": "Container image",
|
|
114
|
-
"runtime-config": "Runtime config",
|
|
115
|
-
"declared-doc": "Declared in docs",
|
|
116
|
-
},
|
|
117
|
-
"interfaceKind": {
|
|
118
|
-
"port": "Port",
|
|
119
|
-
"adapter": "Adapter",
|
|
120
|
-
"service-api": "Service API",
|
|
121
|
-
"module-export": "Module export",
|
|
122
|
-
"event": "Event",
|
|
123
|
-
},
|
|
124
|
-
"toolKind": {
|
|
125
|
-
"packageManager": "Package manager",
|
|
126
|
-
"build": "Build",
|
|
127
|
-
"test": "Test",
|
|
128
|
-
"lint": "Lint",
|
|
129
|
-
"typecheck": "Type check",
|
|
130
|
-
"format": "Format",
|
|
131
|
-
"ci": "CI",
|
|
132
|
-
"container": "Container",
|
|
133
|
-
},
|
|
134
|
-
"lens": {"candidate-found": "Candidate found", "no-candidate": "Nothing found"},
|
|
135
|
-
"rejectionSource": {
|
|
136
|
-
"brief-desired-outcome": "Brief · desired outcome",
|
|
137
|
-
"brief-out-of-scope": "Brief · out of scope",
|
|
138
|
-
"brief-source-material": "Brief · source material",
|
|
139
|
-
"clarification-raised": "Raised as a question",
|
|
140
|
-
"absent-not-material": "Absent · does not change the call",
|
|
141
|
-
},
|
|
142
|
-
"termSource": {
|
|
143
|
-
"glossary": "Glossary",
|
|
144
|
-
"decision-record": "Decision record",
|
|
145
|
-
"brief": "Brief",
|
|
146
|
-
"clarification": "Your answer",
|
|
147
|
-
},
|
|
148
|
-
"handoffMode": {"whole-task": "Whole task", "stage-group": "Selected stages"},
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# What each member of a vocabulary actually claims. A two-word label fits a
|
|
153
|
-
# table cell but cannot carry a definition, and a reader meeting "Passes
|
|
154
|
-
# without proving" for the first time has nowhere to look it up. Only
|
|
155
|
-
# vocabularies whose members are judgements need one; a kind names itself.
|
|
156
|
-
ENUM_HINTS: dict[str, dict[str, str]] = {
|
|
157
|
-
"coverage": {
|
|
158
|
-
"covered": "Break this area and a test fails.",
|
|
159
|
-
"partial": "Tests exist, but part of the area runs through none of them.",
|
|
160
|
-
"gap": "No test runs through this area at all.",
|
|
161
|
-
"risk": (
|
|
162
|
-
"Tests run and pass, but they skip or assert nothing, so passing "
|
|
163
|
-
"is not evidence that the area works. More dangerous than "
|
|
164
|
-
"'No test', because the suite reports success."
|
|
165
|
-
),
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def enum_label(value: object, vocabulary: str) -> str:
|
|
100
|
+
# The tables live in `templates/reports/html/i18n/<lang>.json` beside the rest
|
|
101
|
+
# of the page's fixed strings — a label is chrome, not data, so it follows the
|
|
102
|
+
# reader's language rather than the SSOT's. A value with no entry renders as
|
|
103
|
+
# itself: a new enum member shows up raw rather than silently reading as
|
|
104
|
+
# something it is not.
|
|
105
|
+
def enum_label(value: object, vocabulary: str, chrome: dict) -> str:
|
|
171
106
|
"""Render a schema enum as the words it stands for."""
|
|
172
107
|
raw = "" if value is None else str(value)
|
|
173
|
-
return
|
|
108
|
+
return (chrome.get("enum") or {}).get(vocabulary, {}).get(raw, raw)
|
|
174
109
|
|
|
175
110
|
|
|
176
|
-
def enum_legend(vocabulary: str) -> list[tuple[str, str]]:
|
|
111
|
+
def enum_legend(vocabulary: str, chrome: dict) -> list[tuple[str, str]]:
|
|
177
112
|
"""Pair each label with what it claims, for a legend beside the table.
|
|
178
113
|
|
|
179
|
-
|
|
180
|
-
|
|
114
|
+
A two-word label fits a table cell but cannot carry a definition, and a
|
|
115
|
+
reader meeting "Passes without proving" for the first time has nowhere to
|
|
116
|
+
look it up. Built from the same dictionary the cells render from, so a
|
|
117
|
+
legend cannot drift from the labels it explains.
|
|
181
118
|
"""
|
|
182
|
-
labels =
|
|
183
|
-
|
|
119
|
+
labels = (chrome.get("enum") or {}).get(vocabulary, {})
|
|
120
|
+
hints = (chrome.get("enumHint") or {}).get(vocabulary, {})
|
|
121
|
+
return [(labels.get(value, value), hint) for value, hint in hints.items()]
|
|
184
122
|
|
|
185
123
|
|
|
186
124
|
def code_evidence(rows: object) -> Markup:
|
|
@@ -3,12 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import json
|
|
6
|
+
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import okstra_vendor # noqa: F401 # registers vendored dependency aliases
|
|
9
10
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
|
|
10
11
|
|
|
12
|
+
from ..final_report_paths import translation_sidecar_path
|
|
11
13
|
from ..final_report_schema import load_schema_for_data, validate
|
|
14
|
+
from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
|
|
15
|
+
from ..report_translation import overlay
|
|
12
16
|
from ..report_view_artifacts import user_responses_dir_for_report
|
|
13
17
|
from .common import anchor_index
|
|
14
18
|
from .filters import (
|
|
@@ -64,6 +68,39 @@ def _report_meta(data: dict, run_meta: HtmlRunMeta) -> dict[str, object]:
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
|
|
71
|
+
def _localize(data: dict, data_path: Path) -> tuple[dict, str]:
|
|
72
|
+
"""Apply the translation sidecar for this report's language, if there is one.
|
|
73
|
+
|
|
74
|
+
The data.json is the English SSOT and is never rewritten — the sidecar is
|
|
75
|
+
read here and overlaid onto an in-memory copy, so only this HTML document
|
|
76
|
+
speaks the reader's language. A pointer the sidecar left untranslated
|
|
77
|
+
renders in English; that keeps a half-finished translation readable, and
|
|
78
|
+
the counts are printed because a silently half-empty sidecar otherwise
|
|
79
|
+
ships looking finished.
|
|
80
|
+
"""
|
|
81
|
+
lang = str((data.get("meta") or {}).get("reportLanguage") or "en")
|
|
82
|
+
if lang == "en":
|
|
83
|
+
return data, lang
|
|
84
|
+
sidecar_file = translation_sidecar_path(data_path, lang)
|
|
85
|
+
if not sidecar_file.is_file():
|
|
86
|
+
sys.stdout.write(
|
|
87
|
+
f"note: no {lang} translation sidecar at {sidecar_file.name}; "
|
|
88
|
+
"rendering the English source\n"
|
|
89
|
+
)
|
|
90
|
+
return data, lang
|
|
91
|
+
payload = json.loads(sidecar_file.read_text(encoding="utf-8"))
|
|
92
|
+
strings = payload.get("strings")
|
|
93
|
+
if not isinstance(strings, dict):
|
|
94
|
+
raise HtmlRenderError(f"translation sidecar has no 'strings' object: {sidecar_file}")
|
|
95
|
+
localized, report = overlay(data, strings)
|
|
96
|
+
sys.stdout.write(
|
|
97
|
+
f"translated {report.applied} string(s) into {lang}"
|
|
98
|
+
f" ({len(report.untranslated)} left in English"
|
|
99
|
+
f", {len(report.unresolved)} unresolved)\n"
|
|
100
|
+
)
|
|
101
|
+
return localized, lang
|
|
102
|
+
|
|
103
|
+
|
|
67
104
|
def _html_path(data_path: Path) -> Path:
|
|
68
105
|
suffix = ".data.json"
|
|
69
106
|
if not data_path.name.endswith(suffix):
|
|
@@ -84,6 +121,9 @@ def render_v2_html_view(
|
|
|
84
121
|
raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
|
|
85
122
|
if not markdown_path.is_file():
|
|
86
123
|
raise HtmlRenderError(f"v2 markdown sibling not found: {markdown_path}")
|
|
124
|
+
# Validate the SSOT, then localize — the sidecar carries presentation and
|
|
125
|
+
# has no say in whether the report is well-formed.
|
|
126
|
+
data, lang = _localize(data, data_path)
|
|
87
127
|
route = resolve_html_route(run_meta.task_type)
|
|
88
128
|
view = route.view_builder(data)
|
|
89
129
|
root = _templates_root(templates_root)
|
|
@@ -92,9 +132,11 @@ def render_v2_html_view(
|
|
|
92
132
|
# Binding the index here is what lets a template cite an id without
|
|
93
133
|
# threading the index through every macro and call site.
|
|
94
134
|
anchors = anchor_index(data)
|
|
135
|
+
chrome = load_dictionary(lang, HTML_DICTIONARY_REL)
|
|
136
|
+
env.globals["t"] = make_jinja_global(chrome)
|
|
95
137
|
env.filters["code_evidence"] = code_evidence
|
|
96
|
-
env.filters["enum_label"] = enum_label
|
|
97
|
-
env.filters["enum_legend"] = enum_legend
|
|
138
|
+
env.filters["enum_label"] = lambda value, vocabulary: enum_label(value, vocabulary, chrome)
|
|
139
|
+
env.filters["enum_legend"] = lambda vocabulary: enum_legend(vocabulary, chrome)
|
|
98
140
|
env.filters["evidence_refs"] = lambda refs: evidence_refs(refs, anchors)
|
|
99
141
|
env.filters["inline_code"] = lambda value: inline_code(value, anchors)
|
|
100
142
|
env.filters["paragraphs"] = lambda value: paragraphs(value, anchors)
|