okstra 0.34.1 → 0.36.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.kr.md +26 -16
- package/README.md +26 -16
- package/docs/kr/architecture.md +59 -45
- package/docs/kr/cli.md +61 -18
- package/docs/pr-template-usage.md +65 -0
- package/docs/project-structure-overview.md +358 -354
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
- package/docs/task-process/README.md +74 -0
- package/docs/task-process/common-flow.md +166 -0
- package/docs/task-process/error-analysis.md +101 -0
- package/docs/task-process/final-verification.md +167 -0
- package/docs/task-process/implementation-planning.md +128 -0
- package/docs/task-process/implementation.md +149 -0
- package/docs/task-process/release-handoff.md +206 -0
- package/docs/task-process/requirements-discovery.md +115 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +12 -2
- package/runtime/agents/workers/claude-worker.md +26 -0
- package/runtime/agents/workers/codex-worker.md +27 -1
- package/runtime/agents/workers/gemini-worker.md +27 -1
- package/runtime/agents/workers/report-writer-worker.md +8 -1
- package/runtime/bin/okstra-central.sh +6 -6
- package/runtime/bin/okstra-codex-exec.sh +49 -28
- package/runtime/bin/okstra-gemini-exec.sh +39 -21
- package/runtime/bin/okstra-render-final-report.py +13 -2
- package/runtime/bin/okstra-wrapper-status.py +155 -0
- package/runtime/bin/okstra.sh +2 -2
- package/runtime/prompts/profiles/_common-contract.md +11 -6
- package/runtime/prompts/profiles/error-analysis.md +3 -7
- package/runtime/prompts/profiles/implementation-planning.md +22 -21
- package/runtime/prompts/profiles/implementation.md +28 -11
- package/runtime/prompts/profiles/improvement-discovery.md +42 -0
- package/runtime/prompts/profiles/kr/_common-contract.md +92 -0
- package/runtime/prompts/profiles/kr/error-analysis.md +36 -0
- package/runtime/prompts/profiles/kr/final-verification.md +48 -0
- package/runtime/prompts/profiles/kr/implementation-planning.md +90 -0
- package/runtime/prompts/profiles/kr/implementation.md +144 -0
- package/runtime/prompts/profiles/kr/improvement-discovery.md +42 -0
- package/runtime/prompts/profiles/kr/release-handoff.md +104 -0
- package/runtime/prompts/profiles/kr/requirements-discovery.md +42 -0
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -12
- package/runtime/prompts/wizard/prompts.ko.json +230 -0
- package/runtime/python/lib/okstra/cli.sh +2 -49
- package/runtime/python/lib/okstra/globals.sh +21 -21
- package/runtime/python/lib/okstra/interactive.sh +7 -7
- package/runtime/python/okstra_ctl/clarification_items.py +3 -9
- package/runtime/python/okstra_ctl/consumers.py +53 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
- package/runtime/python/okstra_ctl/i18n.py +73 -0
- package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
- package/runtime/python/okstra_ctl/index.py +1 -1
- package/runtime/python/okstra_ctl/paths.py +23 -20
- package/runtime/python/okstra_ctl/render.py +147 -202
- package/runtime/python/okstra_ctl/render_final_report.py +53 -10
- package/runtime/python/okstra_ctl/run.py +292 -107
- package/runtime/python/okstra_ctl/run_context.py +22 -0
- package/runtime/python/okstra_ctl/seeding.py +186 -0
- package/runtime/python/okstra_ctl/wizard.py +348 -127
- package/runtime/python/okstra_ctl/workflow.py +21 -2
- package/runtime/python/okstra_ctl/worktree.py +54 -1
- package/runtime/python/okstra_project/resolver.py +4 -3
- package/runtime/python/okstra_token_usage/report.py +2 -2
- package/runtime/schemas/final-report-v1.0.schema.json +22 -16
- package/runtime/skills/okstra-brief/SKILL.md +124 -31
- package/runtime/skills/okstra-convergence/SKILL.md +2 -3
- package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
- package/runtime/skills/okstra-run/SKILL.md +5 -4
- package/runtime/skills/okstra-schedule/SKILL.md +4 -4
- package/runtime/skills/okstra-setup/SKILL.md +27 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
- package/runtime/templates/okstra.CLAUDE.md +104 -0
- package/runtime/templates/reports/final-report.template.md +93 -98
- package/runtime/templates/reports/i18n/en.json +135 -0
- package/runtime/templates/reports/i18n/ko.json +135 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
- package/runtime/templates/reports/task-brief.template.md +2 -2
- package/runtime/validators/lib/fixtures.sh +30 -0
- package/runtime/validators/lib/runners.sh +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +211 -0
- package/runtime/validators/validate-run.py +121 -26
- package/runtime/validators/validate-workflow.sh +2 -2
- package/runtime/validators/validate_improvement_report.py +275 -0
- package/src/config.mjs +18 -0
- package/src/install.mjs +41 -14
- package/src/setup.mjs +133 -1
- package/src/uninstall.mjs +21 -1
|
@@ -0,0 +1,1501 @@
|
|
|
1
|
+
# Final Report Language Configuration — 구현 계획서
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** okstra final report 의 본문 언어를 `project.json.reportLanguage` 로 설정 가능하게 만들고, 기본값은 영어로 둔다. `templates/reports/final-report.template.md` 의 한국어 fixed text 를 i18n 사전으로 분리해 영문/한국어 두 가지 보고서를 모두 발행한다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** (1) `project.json` 에 `reportLanguage` 옵션 필드 추가 (resolver 가 이미 사용자 필드를 보존하므로 데이터 모델 변경은 사실상 schema 만), (2) `templates/reports/i18n/{en,ko}.json` 사전 신설, (3) `render_final_report.py` 에 i18n lookup 함수 `t("dotted.key")` 를 Jinja2 글로벌로 주입 (누락 키는 즉시 `RuntimeError`), (4) template 의 한국어 fixed text 를 `{{ t("...") }}` 호출로 모두 교체, (5) lead 가 dispatch 시 `**Report Language:**` 헤더로 worker 에 전달.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.10+, vendored Jinja2 (`okstra_vendor`), Node 18+ ES modules, JSON 사전 (PyYAML 미도입), pytest, bash e2e.
|
|
10
|
+
|
|
11
|
+
**Spec:** [docs/superpowers/specs/2026-05-20-final-report-language-design.md](../specs/2026-05-20-final-report-language-design.md). 본 계획서는 spec 의 §11 미해결 항목 4개를 다음과 같이 확정한 상태에서 작성한다:
|
|
12
|
+
- Q1 PyYAML 미사용 확인 → **JSON 사전 채택**.
|
|
13
|
+
- Q2 `okstra config get` 존재 확인 → **존재. KEYS dict 에 entry 1 추가로 충분**.
|
|
14
|
+
- Q3 release-handoff 4.6.x i18n 키 네임스페이스 → Task 10 에서 `releaseHandoff.*` 그룹으로 단독 처리.
|
|
15
|
+
- Q4 e2e 시나리오 번호 → Task 13 에서 신규 시퀀스 번호를 `ls tests-e2e/` 결과 기준으로 선정.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 파일 구조
|
|
20
|
+
|
|
21
|
+
| 경로 | 책임 | 신규/수정 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `templates/reports/i18n/en.json` | 영문 fixed text 사전 | 신규 |
|
|
24
|
+
| `templates/reports/i18n/ko.json` | 한국어 fixed text 사전 (현행 template 의 한국어 원문이 그대로 들어감) | 신규 |
|
|
25
|
+
| `scripts/okstra_ctl/render_final_report.py` | `t(key, lang)` lookup + Jinja2 글로벌 등록, `report_language` 파라미터 라우팅 | 수정 |
|
|
26
|
+
| `scripts/okstra-render-final-report.py` | `--report-language en|ko` CLI flag, data.json `meta.reportLanguage` 폴백 | 수정 |
|
|
27
|
+
| `templates/reports/final-report.template.md` | 한국어 fixed text 를 `{{ t("...") }}` 로 교체 | 수정 (대량) |
|
|
28
|
+
| `schemas/final-report-v1.0.schema.json` | `meta.reportLanguage` ∈ `{"en","ko"}` 필수 필드 추가 | 수정 |
|
|
29
|
+
| `scripts/okstra_project/resolver.py` | 사용자 보존 필드 주석에 `reportLanguage` 명시 (기능 변경 없음) | 수정 (주석만) |
|
|
30
|
+
| `src/config.mjs` | `KEYS["report-language"]` entry 추가, validator (en/ko/auto 만 허용) | 수정 |
|
|
31
|
+
| `skills/okstra-setup/SKILL.md` | Step 4.9 신설 | 수정 |
|
|
32
|
+
| `skills/okstra-report-writer/SKILL.md` | 한국어 hardcode 2 줄 교체 + dispatch prompt header + `meta.reportLanguage` 계약 | 수정 |
|
|
33
|
+
| `agents/SKILL.md` | Phase 6 prep 체크리스트에 reportLanguage 해상 + line 322 한국어 교체 | 수정 |
|
|
34
|
+
| `agents/workers/report-writer-worker.md` | reading set 에 `templates/reports/i18n/*.json` + `meta.reportLanguage` reminder | 수정 |
|
|
35
|
+
| `tests/test_report_language_i18n.py` | 사전 lookup, 키셋 일치, 누락 키 RuntimeError | 신규 |
|
|
36
|
+
| `tests/test_report_language_render.py` | renderer 의 lang 라우팅, schema 검증 | 신규 |
|
|
37
|
+
| `tests/test_report_language_resolver.py` | resolver 가 reportLanguage 를 보존하는지 회귀 | 신규 |
|
|
38
|
+
| `tests/test_config_report_language.js` 또는 npm test 흐름 | `okstra config set report-language` validator | 신규 |
|
|
39
|
+
| `tests-e2e/scenario-NN-final-report-en.sh` | 영문 final report 전체 흐름 | 신규 |
|
|
40
|
+
| `tests-e2e/scenario-MM-final-report-ko.sh` | 한국어 final report 전체 흐름 | 신규 |
|
|
41
|
+
| `CHANGES.md` | `사용자 영향:` 항목 1 개 추가 | 수정 |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Task 1: i18n 사전 인프라 — JSON 사전 + lookup helper
|
|
46
|
+
|
|
47
|
+
**Files:**
|
|
48
|
+
- Create: `templates/reports/i18n/en.json`
|
|
49
|
+
- Create: `templates/reports/i18n/ko.json`
|
|
50
|
+
- Create: `scripts/okstra_ctl/i18n.py`
|
|
51
|
+
- Test: `tests/test_report_language_i18n.py`
|
|
52
|
+
|
|
53
|
+
본 작업은 lookup 함수와 두 사전의 골격만 만든다. 실제 key 충전은 Task 6–10 에서 카테고리별로 진행한다.
|
|
54
|
+
|
|
55
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
56
|
+
|
|
57
|
+
`tests/test_report_language_i18n.py`:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import json
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
import pytest
|
|
63
|
+
|
|
64
|
+
from okstra_ctl.i18n import I18nError, load_dictionary, lookup, make_jinja_global
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_load_dictionary_returns_dict_for_en():
|
|
71
|
+
d = load_dictionary("en")
|
|
72
|
+
assert isinstance(d, dict)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_load_dictionary_rejects_unknown_lang():
|
|
76
|
+
with pytest.raises(I18nError):
|
|
77
|
+
load_dictionary("fr")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_lookup_returns_value_for_existing_dotted_key():
|
|
81
|
+
d = {"foo": {"bar": "hello"}}
|
|
82
|
+
assert lookup(d, "foo.bar") == "hello"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_lookup_raises_for_missing_key():
|
|
86
|
+
d = {"foo": {"bar": "hello"}}
|
|
87
|
+
with pytest.raises(I18nError) as exc:
|
|
88
|
+
lookup(d, "foo.baz")
|
|
89
|
+
assert "foo.baz" in str(exc.value)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_lookup_raises_when_intermediate_is_not_dict():
|
|
93
|
+
d = {"foo": "leaf-string"}
|
|
94
|
+
with pytest.raises(I18nError):
|
|
95
|
+
lookup(d, "foo.bar")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_en_and_ko_keysets_must_match():
|
|
99
|
+
en = load_dictionary("en")
|
|
100
|
+
ko = load_dictionary("ko")
|
|
101
|
+
|
|
102
|
+
def flatten(d, prefix=""):
|
|
103
|
+
keys = set()
|
|
104
|
+
for k, v in d.items():
|
|
105
|
+
path = f"{prefix}.{k}" if prefix else k
|
|
106
|
+
if isinstance(v, dict):
|
|
107
|
+
keys |= flatten(v, path)
|
|
108
|
+
else:
|
|
109
|
+
keys.add(path)
|
|
110
|
+
return keys
|
|
111
|
+
|
|
112
|
+
en_keys = flatten(en)
|
|
113
|
+
ko_keys = flatten(ko)
|
|
114
|
+
missing_in_ko = en_keys - ko_keys
|
|
115
|
+
missing_in_en = ko_keys - en_keys
|
|
116
|
+
assert missing_in_ko == set(), f"keys present in en but missing in ko: {sorted(missing_in_ko)}"
|
|
117
|
+
assert missing_in_en == set(), f"keys present in ko but missing in en: {sorted(missing_in_en)}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_make_jinja_global_raises_on_missing_key():
|
|
121
|
+
d = {"foo": {"bar": "hello"}}
|
|
122
|
+
t = make_jinja_global(d)
|
|
123
|
+
assert t("foo.bar") == "hello"
|
|
124
|
+
with pytest.raises(I18nError):
|
|
125
|
+
t("nope.nada")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
129
|
+
|
|
130
|
+
Run: `python3 -m pytest tests/test_report_language_i18n.py -v`
|
|
131
|
+
Expected: 모든 케이스가 `ModuleNotFoundError: okstra_ctl.i18n` 으로 실패.
|
|
132
|
+
|
|
133
|
+
- [ ] **Step 3: `scripts/okstra_ctl/i18n.py` 구현**
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
"""Final-report i18n dictionary loader + Jinja2 lookup function.
|
|
137
|
+
|
|
138
|
+
사전은 ``templates/reports/i18n/<lang>.json`` 에 둔다. ChainableUndefined
|
|
139
|
+
환경에서도 누락 키가 silent 로 빈 문자열이 되지 않도록 lookup 함수가
|
|
140
|
+
직접 raise 한다.
|
|
141
|
+
"""
|
|
142
|
+
from __future__ import annotations
|
|
143
|
+
|
|
144
|
+
import json
|
|
145
|
+
import os
|
|
146
|
+
from pathlib import Path
|
|
147
|
+
from typing import Any, Callable
|
|
148
|
+
|
|
149
|
+
SUPPORTED_LANGS = ("en", "ko")
|
|
150
|
+
DICTIONARY_REL = ("templates", "reports", "i18n")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class I18nError(RuntimeError):
|
|
154
|
+
"""사전 lookup 실패 또는 사전 로드 실패."""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _i18n_dir() -> Path:
|
|
158
|
+
okstra_home = os.environ.get("OKSTRA_HOME")
|
|
159
|
+
if okstra_home:
|
|
160
|
+
candidate = Path(okstra_home).joinpath(*DICTIONARY_REL)
|
|
161
|
+
if candidate.is_dir():
|
|
162
|
+
return candidate
|
|
163
|
+
here = Path(__file__).resolve()
|
|
164
|
+
for parent in [here, *here.parents]:
|
|
165
|
+
candidate = parent.joinpath(*DICTIONARY_REL)
|
|
166
|
+
if candidate.is_dir():
|
|
167
|
+
return candidate
|
|
168
|
+
raise I18nError(
|
|
169
|
+
"could not locate templates/reports/i18n/. Set OKSTRA_HOME or "
|
|
170
|
+
"run from a checkout that contains templates/reports/i18n/."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def load_dictionary(lang: str) -> dict[str, Any]:
|
|
175
|
+
if lang not in SUPPORTED_LANGS:
|
|
176
|
+
raise I18nError(
|
|
177
|
+
f"unsupported reportLanguage {lang!r}; supported: {SUPPORTED_LANGS}"
|
|
178
|
+
)
|
|
179
|
+
path = _i18n_dir() / f"{lang}.json"
|
|
180
|
+
try:
|
|
181
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
182
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
183
|
+
raise I18nError(f"failed to load {path}: {exc}") from exc
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def lookup(dictionary: dict[str, Any], dotted_key: str) -> str:
|
|
187
|
+
parts = dotted_key.split(".")
|
|
188
|
+
cur: Any = dictionary
|
|
189
|
+
for i, part in enumerate(parts):
|
|
190
|
+
if not isinstance(cur, dict):
|
|
191
|
+
raise I18nError(
|
|
192
|
+
f"i18n key {dotted_key!r}: segment {'.'.join(parts[:i]) or '<root>'!r} "
|
|
193
|
+
f"is not a dict (got {type(cur).__name__})"
|
|
194
|
+
)
|
|
195
|
+
if part not in cur:
|
|
196
|
+
raise I18nError(f"i18n key {dotted_key!r} not found in dictionary")
|
|
197
|
+
cur = cur[part]
|
|
198
|
+
if not isinstance(cur, str):
|
|
199
|
+
raise I18nError(
|
|
200
|
+
f"i18n key {dotted_key!r} resolved to {type(cur).__name__}, expected str"
|
|
201
|
+
)
|
|
202
|
+
return cur
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def make_jinja_global(dictionary: dict[str, Any]) -> Callable[[str], str]:
|
|
206
|
+
def t(dotted_key: str) -> str:
|
|
207
|
+
return lookup(dictionary, dotted_key)
|
|
208
|
+
return t
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- [ ] **Step 4: 빈 사전 골격 작성**
|
|
212
|
+
|
|
213
|
+
`templates/reports/en.json` 이 아니라 `templates/reports/i18n/en.json`. 디렉터리도 함께 만든다.
|
|
214
|
+
|
|
215
|
+
`templates/reports/i18n/en.json`:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"_meta": {
|
|
220
|
+
"lang": "en",
|
|
221
|
+
"note": "okstra final-report fixed strings (English). Keys must mirror ko.json exactly. Add a key here AND in ko.json in the same commit."
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`templates/reports/i18n/ko.json`:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"_meta": {
|
|
231
|
+
"lang": "ko",
|
|
232
|
+
"note": "okstra final-report 고정 문자열 (한국어). 키는 en.json 과 정확히 일치해야 한다. 키를 추가하면 en.json 도 같은 커밋에서 추가한다."
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- [ ] **Step 5: 테스트 통과 확인**
|
|
238
|
+
|
|
239
|
+
Run: `python3 -m pytest tests/test_report_language_i18n.py -v`
|
|
240
|
+
Expected: 7 passed.
|
|
241
|
+
|
|
242
|
+
- [ ] **Step 6: 커밋**
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
git add scripts/okstra_ctl/i18n.py templates/reports/i18n/en.json templates/reports/i18n/ko.json tests/test_report_language_i18n.py
|
|
246
|
+
git commit -m "feat(i18n): add final-report dictionary loader and lookup function"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Task 2: render_final_report 에 i18n 라우팅 통합
|
|
252
|
+
|
|
253
|
+
**Files:**
|
|
254
|
+
- Modify: `scripts/okstra_ctl/render_final_report.py`
|
|
255
|
+
- Modify: `scripts/okstra-render-final-report.py`
|
|
256
|
+
- Test: `tests/test_report_language_render.py`
|
|
257
|
+
|
|
258
|
+
기존 `render(data, *, template_path)` 시그니처에 `report_language: str | None = None` 키워드를 추가하고, 해상 우선순위(인자 > `data["meta"]["reportLanguage"]` > 영문 폴백)를 한 곳에 두는 함수를 만든다.
|
|
259
|
+
|
|
260
|
+
- [ ] **Step 1: 실패 테스트 작성**
|
|
261
|
+
|
|
262
|
+
`tests/test_report_language_render.py`:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
"""renderer i18n 라우팅 회귀 테스트."""
|
|
266
|
+
import json
|
|
267
|
+
from pathlib import Path
|
|
268
|
+
import pytest
|
|
269
|
+
|
|
270
|
+
from okstra_ctl.render_final_report import (
|
|
271
|
+
FinalReportRenderError,
|
|
272
|
+
resolve_report_language,
|
|
273
|
+
render,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_resolve_arg_wins_over_meta():
|
|
278
|
+
data = {"meta": {"reportLanguage": "ko"}}
|
|
279
|
+
assert resolve_report_language(data, override="en") == "en"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_resolve_meta_used_when_no_override():
|
|
283
|
+
data = {"meta": {"reportLanguage": "ko"}}
|
|
284
|
+
assert resolve_report_language(data, override=None) == "ko"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_resolve_falls_back_to_en_when_neither_present():
|
|
288
|
+
assert resolve_report_language({}, override=None) == "en"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_resolve_rejects_invalid_value():
|
|
292
|
+
with pytest.raises(FinalReportRenderError):
|
|
293
|
+
resolve_report_language({}, override="fr")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_resolve_rejects_auto_at_renderer_level():
|
|
297
|
+
"""auto 는 lead 가 미리 해상해야 한다. renderer 에 도달하면 에러."""
|
|
298
|
+
with pytest.raises(FinalReportRenderError):
|
|
299
|
+
resolve_report_language({"meta": {"reportLanguage": "auto"}}, override=None)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_render_uses_t_function_from_template(tmp_path):
|
|
303
|
+
"""template 안에서 {{ t("foo.bar") }} 가 사전 값으로 치환되는지."""
|
|
304
|
+
template = tmp_path / "tiny.md"
|
|
305
|
+
template.write_text("Hello {{ t('greeting.world') }}.\n")
|
|
306
|
+
# 사전을 OKSTRA_HOME 으로 주입
|
|
307
|
+
i18n_dir = tmp_path / "templates" / "reports" / "i18n"
|
|
308
|
+
i18n_dir.mkdir(parents=True)
|
|
309
|
+
(i18n_dir / "en.json").write_text(json.dumps({"greeting": {"world": "World"}}))
|
|
310
|
+
(i18n_dir / "ko.json").write_text(json.dumps({"greeting": {"world": "세계"}}))
|
|
311
|
+
|
|
312
|
+
import os
|
|
313
|
+
old_home = os.environ.get("OKSTRA_HOME")
|
|
314
|
+
os.environ["OKSTRA_HOME"] = str(tmp_path)
|
|
315
|
+
try:
|
|
316
|
+
out_en = render({"meta": {"reportLanguage": "en"}}, template_path=template)
|
|
317
|
+
out_ko = render({"meta": {"reportLanguage": "ko"}}, template_path=template)
|
|
318
|
+
finally:
|
|
319
|
+
if old_home is None:
|
|
320
|
+
del os.environ["OKSTRA_HOME"]
|
|
321
|
+
else:
|
|
322
|
+
os.environ["OKSTRA_HOME"] = old_home
|
|
323
|
+
|
|
324
|
+
assert out_en.strip() == "Hello World."
|
|
325
|
+
assert out_ko.strip() == "Hello 세계."
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_render_raises_when_template_uses_unknown_i18n_key(tmp_path):
|
|
329
|
+
template = tmp_path / "tiny.md"
|
|
330
|
+
template.write_text("Hello {{ t('nope.nada') }}.\n")
|
|
331
|
+
i18n_dir = tmp_path / "templates" / "reports" / "i18n"
|
|
332
|
+
i18n_dir.mkdir(parents=True)
|
|
333
|
+
(i18n_dir / "en.json").write_text(json.dumps({}))
|
|
334
|
+
(i18n_dir / "ko.json").write_text(json.dumps({}))
|
|
335
|
+
|
|
336
|
+
import os
|
|
337
|
+
old_home = os.environ.get("OKSTRA_HOME")
|
|
338
|
+
os.environ["OKSTRA_HOME"] = str(tmp_path)
|
|
339
|
+
try:
|
|
340
|
+
with pytest.raises(FinalReportRenderError):
|
|
341
|
+
render({"meta": {"reportLanguage": "en"}}, template_path=template)
|
|
342
|
+
finally:
|
|
343
|
+
if old_home is None:
|
|
344
|
+
del os.environ["OKSTRA_HOME"]
|
|
345
|
+
else:
|
|
346
|
+
os.environ["OKSTRA_HOME"] = old_home
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
350
|
+
|
|
351
|
+
Run: `python3 -m pytest tests/test_report_language_render.py -v`
|
|
352
|
+
Expected: ImportError (`resolve_report_language` 가 없음).
|
|
353
|
+
|
|
354
|
+
- [ ] **Step 3: `render_final_report.py` 수정 — 해상 함수 + render 시그니처 확장**
|
|
355
|
+
|
|
356
|
+
`scripts/okstra_ctl/render_final_report.py` 에 다음을 추가/수정:
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
# 파일 상단의 import 블록 다음에
|
|
360
|
+
from okstra_ctl.i18n import I18nError, load_dictionary, make_jinja_global
|
|
361
|
+
|
|
362
|
+
SUPPORTED_LANGS = ("en", "ko")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def resolve_report_language(data: dict, *, override: str | None) -> str:
|
|
366
|
+
"""우선순위: override > data.meta.reportLanguage > 'en'."""
|
|
367
|
+
if override is not None:
|
|
368
|
+
candidate = override
|
|
369
|
+
else:
|
|
370
|
+
candidate = (data.get("meta") or {}).get("reportLanguage") or "en"
|
|
371
|
+
if candidate == "auto":
|
|
372
|
+
raise FinalReportRenderError(
|
|
373
|
+
"reportLanguage 'auto' must be resolved by the lead before "
|
|
374
|
+
"the renderer runs; the report-writer worker writes the "
|
|
375
|
+
"resolved 'en' or 'ko' into data.json.meta.reportLanguage."
|
|
376
|
+
)
|
|
377
|
+
if candidate not in SUPPORTED_LANGS:
|
|
378
|
+
raise FinalReportRenderError(
|
|
379
|
+
f"reportLanguage must be one of {SUPPORTED_LANGS}, got {candidate!r}"
|
|
380
|
+
)
|
|
381
|
+
return candidate
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
기존 `render` 함수의 시그니처에 키워드 추가하고 환경에 `t` 글로벌 주입:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
def render(
|
|
388
|
+
data: dict,
|
|
389
|
+
*,
|
|
390
|
+
template_path: Path,
|
|
391
|
+
report_language: str | None = None,
|
|
392
|
+
) -> str:
|
|
393
|
+
if not template_path.is_file():
|
|
394
|
+
raise FinalReportRenderError(f"template not found: {template_path}")
|
|
395
|
+
|
|
396
|
+
lang = resolve_report_language(data, override=report_language)
|
|
397
|
+
try:
|
|
398
|
+
dictionary = load_dictionary(lang)
|
|
399
|
+
except I18nError as exc:
|
|
400
|
+
raise FinalReportRenderError(str(exc)) from exc
|
|
401
|
+
|
|
402
|
+
env = _build_environment(template_path.parent)
|
|
403
|
+
env.globals["t"] = make_jinja_global(dictionary)
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
template = env.get_template(template_path.name)
|
|
407
|
+
return template.render(**data)
|
|
408
|
+
except I18nError as exc:
|
|
409
|
+
raise FinalReportRenderError(
|
|
410
|
+
f"i18n lookup failed while rendering {template_path.name}: {exc}"
|
|
411
|
+
) from exc
|
|
412
|
+
except Exception as exc:
|
|
413
|
+
raise FinalReportRenderError(
|
|
414
|
+
f"render failed for template {template_path.name}: {exc}"
|
|
415
|
+
) from exc
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`render_to_file` 시그니처에도 `report_language` 추가하고 `render` 로 전달:
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
def render_to_file(
|
|
422
|
+
data_path: Path,
|
|
423
|
+
output_path: Path,
|
|
424
|
+
*,
|
|
425
|
+
template_path: Path | None = None,
|
|
426
|
+
report_language: str | None = None,
|
|
427
|
+
) -> int:
|
|
428
|
+
...
|
|
429
|
+
rendered = render(
|
|
430
|
+
data,
|
|
431
|
+
template_path=resolved_template,
|
|
432
|
+
report_language=report_language,
|
|
433
|
+
)
|
|
434
|
+
...
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 4: 테스트 통과 확인**
|
|
438
|
+
|
|
439
|
+
Run: `python3 -m pytest tests/test_report_language_render.py -v`
|
|
440
|
+
Expected: 7 passed.
|
|
441
|
+
|
|
442
|
+
- [ ] **Step 5: CLI 에 `--report-language` 추가**
|
|
443
|
+
|
|
444
|
+
`scripts/okstra-render-final-report.py` 의 `argparse` 블록에 추가:
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
parser.add_argument(
|
|
448
|
+
"--report-language",
|
|
449
|
+
choices=["en", "ko"],
|
|
450
|
+
default=None,
|
|
451
|
+
help=(
|
|
452
|
+
"Override the language passed into the renderer. When omitted, "
|
|
453
|
+
"the renderer reads data.json.meta.reportLanguage (fallback 'en')."
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
그리고 `render_to_file` 호출에 전달:
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
bytes_written = render_to_file(
|
|
462
|
+
args.data,
|
|
463
|
+
output,
|
|
464
|
+
template_path=args.template,
|
|
465
|
+
report_language=args.report_language,
|
|
466
|
+
)
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
- [ ] **Step 6: CLI smoke 테스트**
|
|
470
|
+
|
|
471
|
+
Run: `node bin/okstra --version && python3 scripts/okstra-render-final-report.py --help | grep -- --report-language`
|
|
472
|
+
Expected: `--report-language` 옵션이 도움말에 나타남.
|
|
473
|
+
|
|
474
|
+
- [ ] **Step 7: 커밋**
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
git add scripts/okstra_ctl/render_final_report.py scripts/okstra-render-final-report.py tests/test_report_language_render.py
|
|
478
|
+
git commit -m "feat(render): add report-language routing with i18n lookup"
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Task 3: schema 에 `meta.reportLanguage` 추가
|
|
484
|
+
|
|
485
|
+
**Files:**
|
|
486
|
+
- Modify: `schemas/final-report-v1.0.schema.json`
|
|
487
|
+
- Test: `tests/test_report_language_schema.py`
|
|
488
|
+
|
|
489
|
+
- [ ] **Step 1: 기존 schema 의 meta 블록 파악**
|
|
490
|
+
|
|
491
|
+
Run: `python3 -c "import json; s=json.load(open('schemas/final-report-v1.0.schema.json')); print(list(s.get('properties',{}).keys())); print(s['properties'].get('meta',{}))"`
|
|
492
|
+
이 출력으로 기존 `meta` 가 있는지 확인. 없으면 새 properties 로 추가, 있으면 nested.
|
|
493
|
+
|
|
494
|
+
- [ ] **Step 2: 실패 테스트 작성**
|
|
495
|
+
|
|
496
|
+
`tests/test_report_language_schema.py`:
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
"""schema 에 meta.reportLanguage 가 정의되어 있는지 회귀."""
|
|
500
|
+
import json
|
|
501
|
+
from pathlib import Path
|
|
502
|
+
|
|
503
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
504
|
+
SCHEMA = REPO / "schemas" / "final-report-v1.0.schema.json"
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_schema_has_meta_report_language_enum():
|
|
508
|
+
schema = json.loads(SCHEMA.read_text())
|
|
509
|
+
meta = schema["properties"]["meta"]
|
|
510
|
+
rl = meta["properties"]["reportLanguage"]
|
|
511
|
+
assert rl["type"] == "string"
|
|
512
|
+
assert sorted(rl["enum"]) == ["en", "ko"]
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_schema_meta_report_language_is_required():
|
|
516
|
+
schema = json.loads(SCHEMA.read_text())
|
|
517
|
+
meta = schema["properties"]["meta"]
|
|
518
|
+
assert "reportLanguage" in meta.get("required", [])
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- [ ] **Step 3: 테스트 실패 확인**
|
|
522
|
+
|
|
523
|
+
Run: `python3 -m pytest tests/test_report_language_schema.py -v`
|
|
524
|
+
Expected: KeyError 또는 AssertionError.
|
|
525
|
+
|
|
526
|
+
- [ ] **Step 4: schema patch**
|
|
527
|
+
|
|
528
|
+
`schemas/final-report-v1.0.schema.json` 의 `properties.meta.properties` 에 추가:
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
"reportLanguage": {
|
|
532
|
+
"type": "string",
|
|
533
|
+
"enum": ["en", "ko"],
|
|
534
|
+
"description": "Resolved language used for prose + i18n dictionary. 'auto' must be resolved to 'en' or 'ko' by the lead before the report-writer worker writes data.json."
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
그리고 `properties.meta.required` 배열에 `"reportLanguage"` 추가.
|
|
539
|
+
|
|
540
|
+
만약 `properties.meta` 가 schema 에 아직 없으면 새 객체로 만든다:
|
|
541
|
+
|
|
542
|
+
```json
|
|
543
|
+
"meta": {
|
|
544
|
+
"type": "object",
|
|
545
|
+
"additionalProperties": true,
|
|
546
|
+
"required": ["reportLanguage"],
|
|
547
|
+
"properties": {
|
|
548
|
+
"reportLanguage": { ... 위 정의 ... }
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
- [ ] **Step 5: 테스트 통과 확인**
|
|
554
|
+
|
|
555
|
+
Run: `python3 -m pytest tests/test_report_language_schema.py -v`
|
|
556
|
+
Expected: 2 passed.
|
|
557
|
+
|
|
558
|
+
- [ ] **Step 6: 기존 validator 회귀 확인**
|
|
559
|
+
|
|
560
|
+
Run: `python3 -m pytest tests/ -k "validate or schema" -v`
|
|
561
|
+
Expected: 기존 테스트가 모두 통과해야 한다. 만약 기존 fixture data.json 이 `meta.reportLanguage` 없이 사용된다면 fixture 를 보강 (`meta: { reportLanguage: "ko" }`).
|
|
562
|
+
|
|
563
|
+
- [ ] **Step 7: 커밋**
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
git add schemas/final-report-v1.0.schema.json tests/test_report_language_schema.py
|
|
567
|
+
git commit -m "feat(schema): require meta.reportLanguage in final-report v1.0"
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Task 4: resolver 가 reportLanguage 를 보존하는지 회귀 테스트
|
|
573
|
+
|
|
574
|
+
**Files:**
|
|
575
|
+
- Modify: `scripts/okstra_project/resolver.py` (주석만)
|
|
576
|
+
- Test: `tests/test_report_language_resolver.py`
|
|
577
|
+
|
|
578
|
+
resolver 의 `upsert_project_json` 은 이미 사용자 필드를 `dict(data)` 복사로 보존한다. 코드 변경은 없지만 회귀 가드와 주석으로 의도를 명문화한다.
|
|
579
|
+
|
|
580
|
+
- [ ] **Step 1: 회귀 테스트 작성**
|
|
581
|
+
|
|
582
|
+
`tests/test_report_language_resolver.py`:
|
|
583
|
+
|
|
584
|
+
```python
|
|
585
|
+
"""upsert_project_json 이 reportLanguage 사용자 필드를 보존하는지 회귀."""
|
|
586
|
+
import json
|
|
587
|
+
from pathlib import Path
|
|
588
|
+
|
|
589
|
+
import pytest
|
|
590
|
+
|
|
591
|
+
from okstra_project.resolver import upsert_project_json
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def test_upsert_preserves_existing_report_language(tmp_path):
|
|
595
|
+
project_root = tmp_path
|
|
596
|
+
pj_path = project_root / ".project-docs" / "okstra" / "project.json"
|
|
597
|
+
pj_path.parent.mkdir(parents=True)
|
|
598
|
+
pj_path.write_text(json.dumps({
|
|
599
|
+
"projectId": "demo",
|
|
600
|
+
"projectRoot": str(project_root),
|
|
601
|
+
"reportLanguage": "ko",
|
|
602
|
+
"createdAt": "2024-01-01T00:00:00Z",
|
|
603
|
+
"updatedAt": "2024-01-01T00:00:00Z",
|
|
604
|
+
}))
|
|
605
|
+
|
|
606
|
+
result = upsert_project_json(project_root, "demo", now="2026-05-20T00:00:00Z")
|
|
607
|
+
|
|
608
|
+
assert result["reportLanguage"] == "ko"
|
|
609
|
+
|
|
610
|
+
on_disk = json.loads(pj_path.read_text())
|
|
611
|
+
assert on_disk["reportLanguage"] == "ko"
|
|
612
|
+
assert on_disk["updatedAt"] == "2026-05-20T00:00:00Z"
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def test_upsert_does_not_inject_report_language_when_absent(tmp_path):
|
|
616
|
+
"""필드가 없던 프로젝트는 그대로 둔다 (auto 폴백은 runtime 책임)."""
|
|
617
|
+
project_root = tmp_path
|
|
618
|
+
pj_path = project_root / ".project-docs" / "okstra" / "project.json"
|
|
619
|
+
pj_path.parent.mkdir(parents=True)
|
|
620
|
+
pj_path.write_text(json.dumps({
|
|
621
|
+
"projectId": "demo",
|
|
622
|
+
"projectRoot": str(project_root),
|
|
623
|
+
"createdAt": "2024-01-01T00:00:00Z",
|
|
624
|
+
"updatedAt": "2024-01-01T00:00:00Z",
|
|
625
|
+
}))
|
|
626
|
+
|
|
627
|
+
result = upsert_project_json(project_root, "demo", now="2026-05-20T00:00:00Z")
|
|
628
|
+
|
|
629
|
+
assert "reportLanguage" not in result
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
- [ ] **Step 2: 테스트 실행**
|
|
633
|
+
|
|
634
|
+
Run: `python3 -m pytest tests/test_report_language_resolver.py -v`
|
|
635
|
+
Expected: 2 passed (이미 보존 동작이 있으므로 통과해야 한다).
|
|
636
|
+
|
|
637
|
+
- [ ] **Step 3: `resolver.py` 의 사용자 필드 보존 주석 보강**
|
|
638
|
+
|
|
639
|
+
기존 line 120-123 주석 교체:
|
|
640
|
+
|
|
641
|
+
```python
|
|
642
|
+
# Preserve any user-managed fields (e.g. `worktreeSyncDirs`,
|
|
643
|
+
# `qaCommands`, `prTemplatePath`, `reportLanguage`, `mcpServers`) so
|
|
644
|
+
# manual edits to project.json are not wiped by the per-run
|
|
645
|
+
# self-registration upsert. Only the canonical identity/timestamp
|
|
646
|
+
# fields below are owned by this function.
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
- [ ] **Step 4: 커밋**
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
git add scripts/okstra_project/resolver.py tests/test_report_language_resolver.py
|
|
653
|
+
git commit -m "test(resolver): cover reportLanguage preservation across upsert"
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Task 5: `okstra config` 에 `report-language` 키 추가
|
|
659
|
+
|
|
660
|
+
**Files:**
|
|
661
|
+
- Modify: `src/config.mjs`
|
|
662
|
+
- Test: `tests/test_config_report_language.py` (Python 에서 Node CLI 호출 — 기존 테스트가 이 패턴이면 그것을 따른다)
|
|
663
|
+
|
|
664
|
+
`KEYS` dict 에 entry 한 줄 추가. validator 가 `en` / `ko` / `auto` 만 통과시킨다.
|
|
665
|
+
|
|
666
|
+
- [ ] **Step 1: 기존 config 테스트 패턴 확인**
|
|
667
|
+
|
|
668
|
+
Run: `find tests/ -name "*config*" -o -name "*pr_template*" | head && grep -l "okstra config" tests/ -r 2>/dev/null | head`
|
|
669
|
+
|
|
670
|
+
기존에 `pr-template-path` 테스트가 있다면 같은 형태로 작성. 없다면 subprocess 로 `node bin/okstra config set ...` 호출하는 형태로 새로 만든다.
|
|
671
|
+
|
|
672
|
+
- [ ] **Step 2: 실패 테스트 작성**
|
|
673
|
+
|
|
674
|
+
`tests/test_config_report_language.py`:
|
|
675
|
+
|
|
676
|
+
```python
|
|
677
|
+
"""okstra config set/get report-language 통합 테스트."""
|
|
678
|
+
import json
|
|
679
|
+
import os
|
|
680
|
+
import subprocess
|
|
681
|
+
from pathlib import Path
|
|
682
|
+
|
|
683
|
+
import pytest
|
|
684
|
+
|
|
685
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
686
|
+
CLI = REPO / "bin" / "okstra"
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _run(args, cwd, env=None):
|
|
690
|
+
full_env = os.environ.copy()
|
|
691
|
+
if env:
|
|
692
|
+
full_env.update(env)
|
|
693
|
+
return subprocess.run(
|
|
694
|
+
["node", str(CLI), *args],
|
|
695
|
+
cwd=str(cwd), capture_output=True, text=True, env=full_env,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@pytest.fixture
|
|
700
|
+
def project(tmp_path):
|
|
701
|
+
root = tmp_path / "proj"
|
|
702
|
+
(root / ".project-docs" / "okstra").mkdir(parents=True)
|
|
703
|
+
(root / ".project-docs" / "okstra" / "project.json").write_text(json.dumps({
|
|
704
|
+
"projectId": "demo",
|
|
705
|
+
"projectRoot": str(root),
|
|
706
|
+
"createdAt": "2024-01-01T00:00:00Z",
|
|
707
|
+
"updatedAt": "2024-01-01T00:00:00Z",
|
|
708
|
+
}))
|
|
709
|
+
return root
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def test_set_en_writes_project_json(project):
|
|
713
|
+
r = _run(["config", "set", "report-language", "en", "--scope", "project"], cwd=project)
|
|
714
|
+
assert r.returncode == 0, r.stderr
|
|
715
|
+
data = json.loads((project / ".project-docs/okstra/project.json").read_text())
|
|
716
|
+
assert data["reportLanguage"] == "en"
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def test_set_ko_writes_project_json(project):
|
|
720
|
+
r = _run(["config", "set", "report-language", "ko", "--scope", "project"], cwd=project)
|
|
721
|
+
assert r.returncode == 0, r.stderr
|
|
722
|
+
data = json.loads((project / ".project-docs/okstra/project.json").read_text())
|
|
723
|
+
assert data["reportLanguage"] == "ko"
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def test_set_auto_writes_project_json(project):
|
|
727
|
+
r = _run(["config", "set", "report-language", "auto", "--scope", "project"], cwd=project)
|
|
728
|
+
assert r.returncode == 0, r.stderr
|
|
729
|
+
data = json.loads((project / ".project-docs/okstra/project.json").read_text())
|
|
730
|
+
assert data["reportLanguage"] == "auto"
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_set_invalid_value_rejected(project):
|
|
734
|
+
r = _run(["config", "set", "report-language", "fr", "--scope", "project"], cwd=project)
|
|
735
|
+
assert r.returncode != 0
|
|
736
|
+
assert "en" in r.stderr or "ko" in r.stderr or "auto" in r.stderr
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def test_get_returns_current_value(project):
|
|
740
|
+
_run(["config", "set", "report-language", "ko", "--scope", "project"], cwd=project)
|
|
741
|
+
r = _run(["config", "get", "report-language", "--scope", "project"], cwd=project)
|
|
742
|
+
assert r.returncode == 0, r.stderr
|
|
743
|
+
assert r.stdout.strip() == "ko"
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def test_set_global_writes_global_config(tmp_path, project):
|
|
747
|
+
home = tmp_path / "okstra_home"
|
|
748
|
+
home.mkdir()
|
|
749
|
+
r = _run(
|
|
750
|
+
["config", "set", "report-language", "en", "--scope", "global"],
|
|
751
|
+
cwd=project,
|
|
752
|
+
env={"OKSTRA_HOME": str(home)},
|
|
753
|
+
)
|
|
754
|
+
assert r.returncode == 0, r.stderr
|
|
755
|
+
data = json.loads((home / "config.json").read_text())
|
|
756
|
+
assert data["reportLanguage"] == "en"
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
- [ ] **Step 3: 테스트 실패 확인**
|
|
760
|
+
|
|
761
|
+
Run: `python3 -m pytest tests/test_config_report_language.py -v`
|
|
762
|
+
Expected: 모든 케이스가 "unknown key 'report-language'" 로 실패.
|
|
763
|
+
|
|
764
|
+
- [ ] **Step 4: `src/config.mjs` 의 KEYS dict 에 entry 추가**
|
|
765
|
+
|
|
766
|
+
기존 `pr-template-path` 다음에 추가:
|
|
767
|
+
|
|
768
|
+
```javascript
|
|
769
|
+
"report-language": {
|
|
770
|
+
jsonField: "reportLanguage",
|
|
771
|
+
scopes: ["project", "global"],
|
|
772
|
+
validate(value) {
|
|
773
|
+
const allowed = new Set(["en", "ko", "auto"]);
|
|
774
|
+
if (typeof value !== "string" || !allowed.has(value)) {
|
|
775
|
+
return (
|
|
776
|
+
`value must be one of: en, ko, auto (got ${JSON.stringify(value)})`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
그리고 USAGE 문자열의 "Supported keys" 블록에 한 줄 추가:
|
|
785
|
+
|
|
786
|
+
```
|
|
787
|
+
report-language -> reportLanguage
|
|
788
|
+
Final-report language. One of: en, ko, auto.
|
|
789
|
+
'auto' is resolved by the report-writer lead from
|
|
790
|
+
the task brief at run time.
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
- [ ] **Step 5: 테스트 통과 확인**
|
|
794
|
+
|
|
795
|
+
Run: `python3 -m pytest tests/test_config_report_language.py -v`
|
|
796
|
+
Expected: 6 passed.
|
|
797
|
+
|
|
798
|
+
- [ ] **Step 6: 커밋**
|
|
799
|
+
|
|
800
|
+
```bash
|
|
801
|
+
git add src/config.mjs tests/test_config_report_language.py
|
|
802
|
+
git commit -m "feat(config): add report-language key (en|ko|auto)"
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## Task 6: template i18n 변환 — 섹션 안내 prose (1차)
|
|
808
|
+
|
|
809
|
+
**Files:**
|
|
810
|
+
- Modify: `templates/reports/final-report.template.md`
|
|
811
|
+
- Modify: `templates/reports/i18n/en.json`
|
|
812
|
+
- Modify: `templates/reports/i18n/ko.json`
|
|
813
|
+
|
|
814
|
+
이번 task 는 line 35, 49, 54, 84, 131, 243, 275, 525 인근의 섹션 안내 prose 를 i18n 키로 옮긴다. 한국어 원문은 그대로 `ko.json` 으로 이동시키고, `en.json` 에는 영문 번역을 채운다. 키 네임스페이스는 `sectionIntro.<section>`.
|
|
815
|
+
|
|
816
|
+
- [ ] **Step 1: 변환 대상 식별**
|
|
817
|
+
|
|
818
|
+
Run: `grep -nE "한눈에 보는|이전 보고서|3~5 개 row|각 worker|어느 워커의|규칙: 한 step|Phase 6 에서 report-writer|다음 run 으로" templates/reports/final-report.template.md`
|
|
819
|
+
|
|
820
|
+
이 출력의 라인 번호를 모두 메모.
|
|
821
|
+
|
|
822
|
+
- [ ] **Step 2: 사전에 키 추가**
|
|
823
|
+
|
|
824
|
+
`templates/reports/i18n/en.json` 의 root 에 추가:
|
|
825
|
+
|
|
826
|
+
```json
|
|
827
|
+
"sectionIntro": {
|
|
828
|
+
"verdictCard": "At-a-glance verdict card. Every value in this table MUST exactly match the authoritative values in `## 2. Final Verdict` and `## 6. Recommended Next Steps`.",
|
|
829
|
+
"clarificationCarryIn": "Walk every C-* row of the prior report's `## 5. Clarification Items` table against new evidence. Update each row's `Status` to `resolved` or `obsolete` and carry it into this run's `## 5.` table. Cite the resolution evidence (file:line / log / worker result) inline.",
|
|
830
|
+
"ticketCoverage": "Summarize 3-5 core problems / requirements / verification targets as a table. Base this on the brief, source material, and worker results.",
|
|
831
|
+
"executionStatus": "Aggregate each worker's status, assigned model, and key findings into one table. Do NOT replace worker artifacts with ungrounded claims.",
|
|
832
|
+
"sourceItemsRule": "`Source items` rule: list which worker items this consensus row was synthesised from, as a comma-list of `<worker>:<item-id>` pairs. Full policy: `prompts/profiles/_common-contract.md` \"Cross-worker traceability\" SSOT.",
|
|
833
|
+
"stepRule": "Rule: each step is roughly 2-5 minutes. Every step MUST include exact file paths and commands.",
|
|
834
|
+
"planBodyVerification": "Result of Phase 6 — report-writer's synthesised 4.5 body re-cast to workers by the lead on a plan-item basis, with verdicts collected.",
|
|
835
|
+
"clarificationItems": "Track items that must be answered by the user or backed by attached material before the next run advances, **all inside one table**."
|
|
836
|
+
},
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
`templates/reports/i18n/ko.json` 의 root 에 같은 키셋, 한국어 값:
|
|
840
|
+
|
|
841
|
+
```json
|
|
842
|
+
"sectionIntro": {
|
|
843
|
+
"verdictCard": "한눈에 보는 결과 카드. 본 표의 모든 값은 `## 2. Final Verdict` 및 `## 6. Recommended Next Steps` 의 권위 있는 값과 정확히 일치해야 합니다.",
|
|
844
|
+
"clarificationCarryIn": "이전 보고서의 `## 5. Clarification Items` 표 매 행(`C-001`, `C-002`, …) 을 새 증거에 비추어 검토하고, 각 행의 `Status` 를 `resolved` 또는 `obsolete` 로 갱신한 뒤 본 run 의 `## 5.` 표에 carry-in 합니다. 해소 근거(파일:라인 / 로그 / 워커 결과) 를 함께 인용합니다.",
|
|
845
|
+
"ticketCoverage": "3~5 개 row 로 핵심 문제·요구사항·검증 대상을 표로 정리합니다. brief, 소스 자료, worker 결과를 근거로 작성합니다.",
|
|
846
|
+
"executionStatus": "각 worker 의 status, 배정 모델, key finding 을 한 표에 모읍니다. worker 산출물을 근거 없는 주장으로 대체하지 않습니다.",
|
|
847
|
+
"sourceItemsRule": "`Source items` 규칙: 본 합의 row 가 어느 워커의 어느 항목들에서 합성됐는지를 `<worker>:<item-id>` 페어 콤마-리스트로 적습니다. 자세한 정책은 `prompts/profiles/_common-contract.md` \"Cross-worker traceability\" SSOT.",
|
|
848
|
+
"stepRule": "규칙: 한 step 은 약 2~5 분. 모든 step 은 정확한 파일 경로와 명령어 포함.",
|
|
849
|
+
"planBodyVerification": "Phase 6 에서 report-writer 가 합성한 4.5 본문을 lead 가 plan-item 단위로 워커들에게 다시 던지고 평결을 수집한 결과.",
|
|
850
|
+
"clarificationItems": "다음 run 으로 넘어가기 전에 사용자가 답하거나 자료를 첨부해야 하는 항목을 **한 표 안에서** 추적합니다."
|
|
851
|
+
},
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
- [ ] **Step 3: template 의 해당 라인을 i18n 호출로 교체**
|
|
855
|
+
|
|
856
|
+
각 라인을 다음 패턴으로 교체:
|
|
857
|
+
|
|
858
|
+
```diff
|
|
859
|
+
- 한눈에 보는 결과 카드. 본 표의 모든 값은 `## 2. Final Verdict` 및 `## 6. Recommended Next Steps` 의 권위 있는 값과 정확히 일치해야 합니다.
|
|
860
|
+
+ {{ t("sectionIntro.verdictCard") }}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
(나머지 7곳도 동일 패턴.)
|
|
864
|
+
|
|
865
|
+
- [ ] **Step 4: 렌더링 회귀 확인**
|
|
866
|
+
|
|
867
|
+
Run: `python3 -m pytest tests/test_report_language_render.py tests/test_report_language_i18n.py -v`
|
|
868
|
+
Expected: 기존 9 + 신규 0 passed.
|
|
869
|
+
|
|
870
|
+
기존 e2e fixture 가 있다면:
|
|
871
|
+
Run: `python3 -m pytest tests/ -k "render or report" -v`
|
|
872
|
+
Expected: 모두 PASS. 실패하면 fixture 에 `meta.reportLanguage` 보강 또는 missing 키 메시지를 읽어 사전에 추가.
|
|
873
|
+
|
|
874
|
+
- [ ] **Step 5: 커밋**
|
|
875
|
+
|
|
876
|
+
```bash
|
|
877
|
+
git add templates/reports/final-report.template.md templates/reports/i18n/en.json templates/reports/i18n/ko.json
|
|
878
|
+
git commit -m "feat(template): i18n section intro prose"
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Task 7: template i18n 변환 — empty-state 라인
|
|
884
|
+
|
|
885
|
+
**Files:**
|
|
886
|
+
- Modify: `templates/reports/final-report.template.md`
|
|
887
|
+
- Modify: `templates/reports/i18n/en.json`
|
|
888
|
+
- Modify: `templates/reports/i18n/ko.json`
|
|
889
|
+
|
|
890
|
+
대상 라인 (spec §5.5 기준): 122, 136, 162, 176, 188, 248, 299, 424, 449, 495, 527, 530, 570. 키 네임스페이스 `emptyState.<concept>`.
|
|
891
|
+
|
|
892
|
+
- [ ] **Step 1: 사전에 키 추가**
|
|
893
|
+
|
|
894
|
+
`en.json` 의 root 에 `emptyState` 그룹:
|
|
895
|
+
|
|
896
|
+
```json
|
|
897
|
+
"emptyState": {
|
|
898
|
+
"consensusItems": "- No consensus items.",
|
|
899
|
+
"differences": "- No significant differences. 1.1 Consensus stands as-is.",
|
|
900
|
+
"primaryEvidence": "- No primary evidence.",
|
|
901
|
+
"secondaryEvidence": "- No secondary evidence or alternate interpretations.",
|
|
902
|
+
"risks": "- No missing information or risks.",
|
|
903
|
+
"dependencyRisk": "- No dependency / migration risks.",
|
|
904
|
+
"dissent": "- No dissenting opinions.",
|
|
905
|
+
"outOfPlanEdits": "- No out-of-plan edits.",
|
|
906
|
+
"declinedFixRecommendations": "- None.",
|
|
907
|
+
"discrepancy": "- None.",
|
|
908
|
+
"lingeringRisks": "- No tracked lingering risks.",
|
|
909
|
+
"noClarification": "- No additional information requested. The Section 2 verdict stands as-is.",
|
|
910
|
+
"noFollowUp": "- No follow-up tasks. The next phase for this run is in §6 (Recommended Next Steps)."
|
|
911
|
+
},
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
`ko.json` 의 root 에 같은 키, 한국어 값 (template 원문 그대로 복사).
|
|
915
|
+
|
|
916
|
+
- [ ] **Step 2: template 라인 교체**
|
|
917
|
+
|
|
918
|
+
각 empty-state 라인을 `{{ t("emptyState.<key>") }}` 로 교체. `{{ block.declinedFixRecommendations or '- 없음.' }}` 같은 표현은 `{{ block.declinedFixRecommendations or t("emptyState.declinedFixRecommendations") }}` 로.
|
|
919
|
+
|
|
920
|
+
- [ ] **Step 3: 회귀 확인**
|
|
921
|
+
|
|
922
|
+
Run: `python3 -m pytest tests/ -k "render or report" -v`
|
|
923
|
+
Expected: PASS.
|
|
924
|
+
|
|
925
|
+
- [ ] **Step 4: 커밋**
|
|
926
|
+
|
|
927
|
+
```bash
|
|
928
|
+
git add templates/reports/final-report.template.md templates/reports/i18n/en.json templates/reports/i18n/ko.json
|
|
929
|
+
git commit -m "feat(template): i18n empty-state lines"
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
934
|
+
## Task 8: template i18n 변환 — 컬럼 헤더 + 헤딩 한국어 부속
|
|
935
|
+
|
|
936
|
+
**Files:**
|
|
937
|
+
- Modify: `templates/reports/final-report.template.md`
|
|
938
|
+
- Modify: `templates/reports/i18n/{en,ko}.json`
|
|
939
|
+
|
|
940
|
+
대상:
|
|
941
|
+
- 컬럼 헤더: line 56 ("한 줄 요약", "출처"), line 86 ("처리 토큰", "환산 토큰", "비용", "Summary of Key Findings" 는 영문이라 제외), line 94 (동일), line 267 ("확인 방법").
|
|
942
|
+
- 헤딩 한국어 부속: `### 4.5.5 Dependency / Migration Risk (의존성·마이그레이션 위험)` 같은 `(...)` 부분 (line 245, 257, 273, 4.5.x 시리즈), `## 0. Clarification Response Carried In` 의 한국어 부속이 있다면.
|
|
943
|
+
|
|
944
|
+
키 네임스페이스: `columns.*`, `sectionAside.*`.
|
|
945
|
+
|
|
946
|
+
- [ ] **Step 1: 사전에 키 추가**
|
|
947
|
+
|
|
948
|
+
`en.json`:
|
|
949
|
+
|
|
950
|
+
```json
|
|
951
|
+
"columns": {
|
|
952
|
+
"summary": "Summary",
|
|
953
|
+
"source": "Source (brief/source/worker)",
|
|
954
|
+
"rawTokens": "Raw tokens",
|
|
955
|
+
"billableTokens": "Billable tokens",
|
|
956
|
+
"billableTokensInputEquiv": "Billable tokens (input-equiv.)",
|
|
957
|
+
"cost": "Cost (USD)",
|
|
958
|
+
"checkMethod": "How to check"
|
|
959
|
+
},
|
|
960
|
+
"sectionAside": {
|
|
961
|
+
"dependencyRisk": "Dependency / Migration Risk",
|
|
962
|
+
"validationChecklist": "Validation Checklist",
|
|
963
|
+
"rollbackStrategy": "Rollback Strategy",
|
|
964
|
+
"planBodyVerification": "Plan Body Verification",
|
|
965
|
+
"recommendedOption": "Recommended Option",
|
|
966
|
+
"optionCandidates": "Option Candidates",
|
|
967
|
+
"tradeOffMatrix": "Trade-off Matrix",
|
|
968
|
+
"stepwiseExecutionOrder": "Stepwise Execution Order"
|
|
969
|
+
},
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
`ko.json` 에 동일 키, 한국어 값 (원문에서 가져옴): `의존성·마이그레이션 위험`, `검증 체크리스트`, `롤백 전략`, `계획 본문 검증`, `권장 옵션`, `옵션 후보`, `트레이드오프 매트릭스`, `단계별 실행 순서`. 컬럼은 `한 줄 요약`, `출처 (brief/source/worker)`, `처리 토큰`, `환산 토큰`, `환산 토큰 (input 기준)`, `비용 (USD)`, `확인 방법`. (토큰 요약 헤딩 "토큰 사용량 요약" 은 Task 9 의 `tokenSummary.heading` 으로 따로 처리.)
|
|
973
|
+
|
|
974
|
+
- [ ] **Step 2: 헤딩 한국어 부속을 조건부 렌더로 교체**
|
|
975
|
+
|
|
976
|
+
영어/한국어가 같은 문자열일 때 빈 괄호가 남지 않도록 조건부 출력:
|
|
977
|
+
|
|
978
|
+
```jinja
|
|
979
|
+
### 4.5.5 Dependency / Migration Risk{% if t("sectionAside.dependencyRisk") != "Dependency / Migration Risk" %} ({{ t("sectionAside.dependencyRisk") }}){% endif %}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
같은 패턴을 다른 헤딩들에도 적용.
|
|
983
|
+
|
|
984
|
+
- [ ] **Step 3: 컬럼 헤더 교체**
|
|
985
|
+
|
|
986
|
+
표 헤더 라인의 한국어 셀을 `{{ t("columns.<key>") }}` 로.
|
|
987
|
+
|
|
988
|
+
- [ ] **Step 4: 회귀 확인**
|
|
989
|
+
|
|
990
|
+
Run: `python3 -m pytest tests/ -k "render or report" -v`
|
|
991
|
+
Expected: PASS.
|
|
992
|
+
|
|
993
|
+
- [ ] **Step 5: 커밋**
|
|
994
|
+
|
|
995
|
+
```bash
|
|
996
|
+
git add templates/reports/final-report.template.md templates/reports/i18n/en.json templates/reports/i18n/ko.json
|
|
997
|
+
git commit -m "feat(template): i18n column headers and heading asides"
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
## Task 9: template i18n 변환 — 토큰 요약 헤딩 + "읽는 법" 블록
|
|
1003
|
+
|
|
1004
|
+
**Files:**
|
|
1005
|
+
- Modify: `templates/reports/final-report.template.md`
|
|
1006
|
+
- Modify: `templates/reports/i18n/{en,ko}.json`
|
|
1007
|
+
|
|
1008
|
+
토큰 요약 섹션은 한 블록 안에 헤딩, 표 헤더, 행 라벨, 그리고 가장 긴 한국어 도움말 ("읽는 법") 이 모여 있다. 한 task 로 묶는다.
|
|
1009
|
+
|
|
1010
|
+
- [ ] **Step 1: 사전에 키 추가**
|
|
1011
|
+
|
|
1012
|
+
`en.json` root 에 `tokenSummary` 그룹:
|
|
1013
|
+
|
|
1014
|
+
```json
|
|
1015
|
+
"tokenSummary": {
|
|
1016
|
+
"heading": "Token Usage Summary",
|
|
1017
|
+
"tableHeaderItem": "Item",
|
|
1018
|
+
"tableHeaderRaw": "Raw tokens",
|
|
1019
|
+
"tableHeaderBillable": "Billable tokens (input-equiv.)",
|
|
1020
|
+
"tableHeaderCost": "Cost (USD)",
|
|
1021
|
+
"rowLead": "Lead",
|
|
1022
|
+
"rowWorkerTotal": "Worker subtotal",
|
|
1023
|
+
"rowGrandTotal": "**Grand total**",
|
|
1024
|
+
"rowCliExtra": "Codex/Gemini CLI add-on",
|
|
1025
|
+
"howToRead": "> **How to read**: \"Raw tokens\" is the sum of input + output + cache_creation + cache_read processed by the model. In long sessions cache_read often exceeds 95% of the total, which inflates the number. \"Billable tokens\" weights cache_read at 0.1×, cache_creation at 1.25×, and output at 5× to give an input-equivalent figure closer to actual cost. Costs are estimates based on public pricing (Anthropic / OpenAI / Google)."
|
|
1026
|
+
},
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
`ko.json` root 에 같은 키, 한국어 (원문에서 가져옴).
|
|
1030
|
+
|
|
1031
|
+
- [ ] **Step 2: template 의 토큰 요약 섹션 교체**
|
|
1032
|
+
|
|
1033
|
+
대략 다음 모양으로:
|
|
1034
|
+
|
|
1035
|
+
```jinja
|
|
1036
|
+
### {{ t("tokenSummary.heading") }}
|
|
1037
|
+
|
|
1038
|
+
| {{ t("tokenSummary.tableHeaderItem") }} | {{ t("tokenSummary.tableHeaderRaw") }} | {{ t("tokenSummary.tableHeaderBillable") }} | {{ t("tokenSummary.tableHeaderCost") }} |
|
|
1039
|
+
|---|---|---|---|
|
|
1040
|
+
| {{ t("tokenSummary.rowLead") }} | ... | ... | ... |
|
|
1041
|
+
...
|
|
1042
|
+
|
|
1043
|
+
{{ t("tokenSummary.howToRead") }}
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
- [ ] **Step 3: 회귀 확인**
|
|
1047
|
+
|
|
1048
|
+
Run: `python3 -m pytest tests/ -k "render or report" -v`
|
|
1049
|
+
Expected: PASS.
|
|
1050
|
+
|
|
1051
|
+
기존 token-usage substitution 테스트(`tests/test_token_usage*.py` 가 있다면) 도 실행:
|
|
1052
|
+
Run: `python3 -m pytest tests/ -k "token" -v`
|
|
1053
|
+
Expected: PASS.
|
|
1054
|
+
|
|
1055
|
+
- [ ] **Step 4: 커밋**
|
|
1056
|
+
|
|
1057
|
+
```bash
|
|
1058
|
+
git add templates/reports/final-report.template.md templates/reports/i18n/en.json templates/reports/i18n/ko.json
|
|
1059
|
+
git commit -m "feat(template): i18n token summary section"
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
---
|
|
1063
|
+
|
|
1064
|
+
## Task 10: template i18n 변환 — release-handoff 4.6.x 라벨
|
|
1065
|
+
|
|
1066
|
+
**Files:**
|
|
1067
|
+
- Modify: `templates/reports/final-report.template.md`
|
|
1068
|
+
- Modify: `templates/reports/i18n/{en,ko}.json`
|
|
1069
|
+
|
|
1070
|
+
대상: line 325 (`기존 PR 존재 여부:`), 329 (표 헤더 `질문 ID`, `질문 본문`, `사용자 응답 (원문)`, `응답이 가능한 보기`), 448-449, 473-474 등 4.6.x 의 한국어 라벨/anchor 텍스트 전체. 키 네임스페이스 `releaseHandoff.*`.
|
|
1071
|
+
|
|
1072
|
+
- [ ] **Step 1: 대상 라인 식별**
|
|
1073
|
+
|
|
1074
|
+
Run: `awk 'NR>=300 && NR<=480' templates/reports/final-report.template.md | grep -nP '[\x{AC00}-\x{D7A3}]'`
|
|
1075
|
+
이 출력의 라인 번호 + 한국어 텍스트 목록을 모두 모은다.
|
|
1076
|
+
|
|
1077
|
+
- [ ] **Step 2: 사전에 키 추가**
|
|
1078
|
+
|
|
1079
|
+
각 한국어 fragment 를 키 하나로 매핑 (`releaseHandoff.existingPrLabel`, `releaseHandoff.questionsTable.questionId`, ... 등). 그룹 구조는 카테고리에 따라:
|
|
1080
|
+
|
|
1081
|
+
```json
|
|
1082
|
+
"releaseHandoff": {
|
|
1083
|
+
"existingPrLabel": "Existing PR:",
|
|
1084
|
+
"userSelections": {
|
|
1085
|
+
"questionsTableHeader": {
|
|
1086
|
+
"questionId": "Question ID",
|
|
1087
|
+
"questionBody": "Question",
|
|
1088
|
+
"userResponse": "User response (raw)",
|
|
1089
|
+
"allowedOptions": "Allowed options"
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
"verification": {
|
|
1093
|
+
"targetWorktreePath": "Target worktree path",
|
|
1094
|
+
"capturedBaseHeadSha": "Base/Head SHA captured at run start"
|
|
1095
|
+
},
|
|
1096
|
+
...
|
|
1097
|
+
},
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
`ko.json` 에 동일 키, 한국어 값 (현행 template 의 원문).
|
|
1101
|
+
|
|
1102
|
+
- [ ] **Step 3: template 의 4.6.x 섹션 교체**
|
|
1103
|
+
|
|
1104
|
+
각 한국어 라벨을 `{{ t("releaseHandoff.<key>") }}` 로.
|
|
1105
|
+
|
|
1106
|
+
- [ ] **Step 4: 회귀 확인**
|
|
1107
|
+
|
|
1108
|
+
Run: `python3 -m pytest tests/ -k "render or report or handoff" -v`
|
|
1109
|
+
Expected: PASS.
|
|
1110
|
+
|
|
1111
|
+
- [ ] **Step 5: 커밋**
|
|
1112
|
+
|
|
1113
|
+
```bash
|
|
1114
|
+
git add templates/reports/final-report.template.md templates/reports/i18n/en.json templates/reports/i18n/ko.json
|
|
1115
|
+
git commit -m "feat(template): i18n release-handoff 4.6.x labels"
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
---
|
|
1119
|
+
|
|
1120
|
+
## Task 11: 한국어 잔존 0 회귀 가드 + 누락 키 검출 통합 테스트
|
|
1121
|
+
|
|
1122
|
+
**Files:**
|
|
1123
|
+
- Test: `tests/test_template_no_korean_glyphs.py`
|
|
1124
|
+
- Test: `tests/test_template_full_render_both_langs.py`
|
|
1125
|
+
|
|
1126
|
+
template 변환 완료 후 한국어 글자가 0개임을 회귀 가드로 묶고, en/ko 양쪽 fixture data 로 전체 render 가 성공하는지 확인한다.
|
|
1127
|
+
|
|
1128
|
+
- [ ] **Step 1: 한국어 글자 0 가드 작성**
|
|
1129
|
+
|
|
1130
|
+
`tests/test_template_no_korean_glyphs.py`:
|
|
1131
|
+
|
|
1132
|
+
```python
|
|
1133
|
+
"""template 에 한국어 글자가 남아 있으면 i18n 화 후 회귀로 간주."""
|
|
1134
|
+
import re
|
|
1135
|
+
from pathlib import Path
|
|
1136
|
+
|
|
1137
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
1138
|
+
TEMPLATE = REPO / "templates" / "reports" / "final-report.template.md"
|
|
1139
|
+
|
|
1140
|
+
KOREAN_RE = re.compile(r"[가-힣]")
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def test_template_has_no_korean_characters():
|
|
1144
|
+
text = TEMPLATE.read_text(encoding="utf-8")
|
|
1145
|
+
matches = [(i + 1, line) for i, line in enumerate(text.splitlines()) if KOREAN_RE.search(line)]
|
|
1146
|
+
assert matches == [], (
|
|
1147
|
+
f"template still contains Korean characters on {len(matches)} lines. "
|
|
1148
|
+
f"i18n these into templates/reports/i18n/<lang>.json: "
|
|
1149
|
+
f"{matches[:5]}{'...' if len(matches) > 5 else ''}"
|
|
1150
|
+
)
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
- [ ] **Step 2: 양 언어 full-render 테스트 작성**
|
|
1154
|
+
|
|
1155
|
+
`tests/test_template_full_render_both_langs.py`:
|
|
1156
|
+
|
|
1157
|
+
```python
|
|
1158
|
+
"""en + ko 양쪽 data fixture 로 template 가 누락 키 없이 렌더되는지."""
|
|
1159
|
+
import json
|
|
1160
|
+
from pathlib import Path
|
|
1161
|
+
|
|
1162
|
+
import pytest
|
|
1163
|
+
|
|
1164
|
+
from okstra_ctl.render_final_report import render, find_default_template
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
@pytest.fixture
|
|
1168
|
+
def base_data():
|
|
1169
|
+
"""가장 작은 schema-conformant data.json 골격. 실제 fixture 가 있으면 그것을 사용."""
|
|
1170
|
+
fx = Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "minimal-final-report.data.json"
|
|
1171
|
+
if fx.is_file():
|
|
1172
|
+
return json.loads(fx.read_text())
|
|
1173
|
+
# 폴백: 인라인 최소 fixture (schema 가 요구하는 필드만 채움 — 변환 시 보강 필요)
|
|
1174
|
+
return {"meta": {"reportLanguage": "en"}}
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def test_render_en(base_data):
|
|
1178
|
+
base_data["meta"]["reportLanguage"] = "en"
|
|
1179
|
+
out = render(base_data, template_path=find_default_template())
|
|
1180
|
+
assert "Token Usage Summary" in out or out # 최소: 예외 없이 끝났는지
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def test_render_ko(base_data):
|
|
1184
|
+
base_data["meta"]["reportLanguage"] = "ko"
|
|
1185
|
+
out = render(base_data, template_path=find_default_template())
|
|
1186
|
+
assert "토큰 사용량 요약" in out or out
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
- [ ] **Step 3: 두 테스트 실행**
|
|
1190
|
+
|
|
1191
|
+
Run: `python3 -m pytest tests/test_template_no_korean_glyphs.py tests/test_template_full_render_both_langs.py -v`
|
|
1192
|
+
Expected: 한국어 글자 0개 통과, 양 언어 렌더 통과. 만약 누락 키 메시지가 나오면 해당 키를 `en.json` / `ko.json` 양쪽에 추가하고 재실행.
|
|
1193
|
+
|
|
1194
|
+
- [ ] **Step 4: 만약 한국어 잔존 라인이 남아 있다면**
|
|
1195
|
+
|
|
1196
|
+
Task 6-10 에서 빠뜨린 fragment 일 가능성. 라인을 보고 적절한 카테고리의 사전에 키를 추가하고 template 를 교체. 모두 0이 될 때까지 반복.
|
|
1197
|
+
|
|
1198
|
+
- [ ] **Step 5: 커밋**
|
|
1199
|
+
|
|
1200
|
+
```bash
|
|
1201
|
+
git add tests/test_template_no_korean_glyphs.py tests/test_template_full_render_both_langs.py
|
|
1202
|
+
git commit -m "test(template): regression guards for i18n completeness"
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
## Task 12: skill / agent 마크다운 변경
|
|
1208
|
+
|
|
1209
|
+
**Files:**
|
|
1210
|
+
- Modify: `skills/okstra-setup/SKILL.md`
|
|
1211
|
+
- Modify: `skills/okstra-report-writer/SKILL.md`
|
|
1212
|
+
- Modify: `agents/SKILL.md`
|
|
1213
|
+
- Modify: `agents/workers/report-writer-worker.md`
|
|
1214
|
+
|
|
1215
|
+
본 task 는 코드 변경 없이 문서/지침만 갱신한다.
|
|
1216
|
+
|
|
1217
|
+
- [ ] **Step 1: `skills/okstra-setup/SKILL.md` — Step 4.9 신설**
|
|
1218
|
+
|
|
1219
|
+
Step 4.8 다음, Step 5 앞에 다음 블록 삽입:
|
|
1220
|
+
|
|
1221
|
+
```markdown
|
|
1222
|
+
## Step 4.9 (optional): choose final report language
|
|
1223
|
+
|
|
1224
|
+
기본은 영어. 이 step 을 skip 하면 reportLanguage 필드를 두지 않고
|
|
1225
|
+
(`auto` 폴백 동작), 사용자가 명시적으로 선택하면 `okstra config set` 으로
|
|
1226
|
+
project.json 에 기록한다.
|
|
1227
|
+
|
|
1228
|
+
AskUserQuestion (fixed options):
|
|
1229
|
+
- Question: `"Final report 를 어느 언어로 작성할까요?"`
|
|
1230
|
+
- Options:
|
|
1231
|
+
1. `English (recommended)` → reportLanguage: "en"
|
|
1232
|
+
2. `한국어` → reportLanguage: "ko"
|
|
1233
|
+
3. `Auto (task brief 언어로 추론, 불분명하면 영어)` → reportLanguage: "auto"
|
|
1234
|
+
4. `나중에` → skip (필드 미설정 → runtime 이 auto 로 처리)
|
|
1235
|
+
|
|
1236
|
+
If the user picks 1/2/3:
|
|
1237
|
+
|
|
1238
|
+
```bash
|
|
1239
|
+
okstra config set report-language <en|ko|auto> --scope project
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
전역 기본값을 두고 싶다면 README 의 "global config" 안내를 따라
|
|
1243
|
+
`--scope global` 로 수동 설정. 이 step 에서는 project 스코프만 제공한다.
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
- [ ] **Step 2: `skills/okstra-report-writer/SKILL.md` — Writing Guidelines 교체**
|
|
1247
|
+
|
|
1248
|
+
```diff
|
|
1249
|
+
- - Write the final report body in Korean.
|
|
1250
|
+
+ - Write the final report body in the language passed in **Report Language**
|
|
1251
|
+
+ above (`en` or `ko`). The template's fixed labels (section asides,
|
|
1252
|
+
+ empty-states, token summary, column headers, release-handoff labels)
|
|
1253
|
+
+ are i18n-rendered by `okstra-render-final-report.py` from
|
|
1254
|
+
+ `templates/reports/i18n/<lang>.json`; do not translate those — focus
|
|
1255
|
+
+ on the prose you author (Section 1 categories, Section 3 evidence
|
|
1256
|
+
+ narratives, Section 4 risks, Section 6 recommendations, etc.).
|
|
1257
|
+
+ Code identifiers, file paths, model names, status tokens, and the
|
|
1258
|
+
+ validator-checked English substrings (`Option Candidates`,
|
|
1259
|
+
+ `Verdict Token`, `accepted`/`conditional-accept`/`blocked`, etc.)
|
|
1260
|
+
+ stay in English regardless of Report Language.
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
```diff
|
|
1264
|
+
- Provide a concise report in Korean covering the following:
|
|
1265
|
+
+ Provide a concise report in the Report Language covering the following:
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
- [ ] **Step 3: `skills/okstra-report-writer/SKILL.md` — Phase 6 dispatch prompt header 추가**
|
|
1269
|
+
|
|
1270
|
+
기존 "The prompt MUST include, in this order at the top:" 목록의 9번 (convergence classifications) 다음에 새 entry 삽입하고 이후 번호를 +1:
|
|
1271
|
+
|
|
1272
|
+
```
|
|
1273
|
+
10. `**Report Language:** <en|ko>` — must be either `en` or `ko`; `auto`
|
|
1274
|
+
has been resolved by the lead from project.json / global config
|
|
1275
|
+
before the dispatch is constructed. The worker copies this verbatim
|
|
1276
|
+
into `data.json.meta.reportLanguage`.
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
- [ ] **Step 4: `skills/okstra-report-writer/SKILL.md` — data.json 계약 단락 추가**
|
|
1280
|
+
|
|
1281
|
+
기존 token-cell null 단락 (line 180 인근, "Set `meta.reportLanguage` ..." 가 없는 위치) 옆에 추가:
|
|
1282
|
+
|
|
1283
|
+
```markdown
|
|
1284
|
+
- Set `meta.reportLanguage` to the resolved `en` or `ko` value passed in
|
|
1285
|
+
**Report Language**. `auto` is forbidden in this field — the lead has
|
|
1286
|
+
already resolved it. The renderer reads this field as SSOT when no
|
|
1287
|
+
CLI `--report-language` flag is given.
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
- [ ] **Step 5: `agents/SKILL.md` — Phase 6 prep 체크리스트 + line 322 교체**
|
|
1291
|
+
|
|
1292
|
+
Phase 6 dispatch 준비 단계에 (lead 가 dispatch prompt 를 구성하기 직전 자리):
|
|
1293
|
+
|
|
1294
|
+
```markdown
|
|
1295
|
+
- Resolve report language: read `project.json.reportLanguage` (fallback
|
|
1296
|
+
`~/.okstra/config.json.reportLanguage`, then literal `auto`). If the
|
|
1297
|
+
resolved value is `auto`, inspect the task brief and pick `en` or `ko`
|
|
1298
|
+
based on its main prose language (default `en` when the brief is
|
|
1299
|
+
mostly code/identifiers). Pass the final `en` or `ko` value as
|
|
1300
|
+
`**Report Language:**` in the report-writer dispatch prompt, and ensure
|
|
1301
|
+
the worker writes the same value into `data.json.meta.reportLanguage`.
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
line 322 교체:
|
|
1305
|
+
|
|
1306
|
+
```diff
|
|
1307
|
+
- After persistence, reply briefly in Korean with: completion status, ...
|
|
1308
|
+
+ After persistence, reply briefly in the resolved Report Language with:
|
|
1309
|
+
+ completion status, ...
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
- [ ] **Step 6: `agents/workers/report-writer-worker.md` — reading set + reminder**
|
|
1313
|
+
|
|
1314
|
+
리딩 셋 목록에 추가:
|
|
1315
|
+
|
|
1316
|
+
```
|
|
1317
|
+
- templates/reports/i18n/en.json
|
|
1318
|
+
- templates/reports/i18n/ko.json
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
본문 적절한 위치에 한 줄 추가:
|
|
1322
|
+
|
|
1323
|
+
```markdown
|
|
1324
|
+
- The `**Report Language:**` header in your dispatch prompt is already
|
|
1325
|
+
resolved to `en` or `ko` by the lead. Copy it verbatim into
|
|
1326
|
+
`data.json.meta.reportLanguage`. Never write `auto` here.
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
- [ ] **Step 7: 변경 검증 — 한국어 hardcode 가 의도된 곳에만 남았는지**
|
|
1330
|
+
|
|
1331
|
+
Run: `grep -nE "in Korean|Korean\." skills/okstra-report-writer/SKILL.md agents/SKILL.md`
|
|
1332
|
+
Expected: 출력에 line 264 / line 322 의 "Korean" hardcode 가 사라지고, 남은 것은 validator 설명 등 의도적 영문 문장.
|
|
1333
|
+
|
|
1334
|
+
- [ ] **Step 8: 커밋**
|
|
1335
|
+
|
|
1336
|
+
```bash
|
|
1337
|
+
git add skills/okstra-setup/SKILL.md skills/okstra-report-writer/SKILL.md agents/SKILL.md agents/workers/report-writer-worker.md
|
|
1338
|
+
git commit -m "docs(skills): wire report-language through setup/lead/report-writer"
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
---
|
|
1342
|
+
|
|
1343
|
+
## Task 13: e2e 시나리오 — en + ko 보고서 발행
|
|
1344
|
+
|
|
1345
|
+
**Files:**
|
|
1346
|
+
- Create: `tests-e2e/scenario-NN-final-report-en.sh`
|
|
1347
|
+
- Create: `tests-e2e/scenario-MM-final-report-ko.sh`
|
|
1348
|
+
|
|
1349
|
+
(NN/MM 는 다음 Step 에서 확정.)
|
|
1350
|
+
|
|
1351
|
+
- [ ] **Step 1: 빈 시나리오 번호 확인**
|
|
1352
|
+
|
|
1353
|
+
Run: `ls tests-e2e/ | sort | tail -10`
|
|
1354
|
+
출력에서 가장 큰 번호를 보고 NN = 그 다음, MM = NN+1 으로 결정.
|
|
1355
|
+
|
|
1356
|
+
- [ ] **Step 2: en 시나리오 작성**
|
|
1357
|
+
|
|
1358
|
+
`tests-e2e/scenario-NN-final-report-en.sh`:
|
|
1359
|
+
|
|
1360
|
+
```bash
|
|
1361
|
+
#!/usr/bin/env bash
|
|
1362
|
+
# E2E: full okstra run with reportLanguage=en. Verifies the rendered
|
|
1363
|
+
# final-report markdown uses the English i18n dictionary.
|
|
1364
|
+
set -euo pipefail
|
|
1365
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
1366
|
+
REPO="$(cd "$HERE/.." && pwd)"
|
|
1367
|
+
TMP="$(mktemp -d -t okstra-e2e-en-XXXXXX)"
|
|
1368
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
1369
|
+
|
|
1370
|
+
export OKSTRA_HOME="$TMP/okstra-home"
|
|
1371
|
+
mkdir -p "$OKSTRA_HOME"
|
|
1372
|
+
|
|
1373
|
+
# Set up a fixture data.json with meta.reportLanguage = "en", render it,
|
|
1374
|
+
# and assert the English token-summary heading appears.
|
|
1375
|
+
FIX="$REPO/tests/fixtures/minimal-final-report.data.json"
|
|
1376
|
+
DATA="$TMP/sample.data.json"
|
|
1377
|
+
python3 -c "
|
|
1378
|
+
import json, sys
|
|
1379
|
+
from pathlib import Path
|
|
1380
|
+
src = Path('$FIX')
|
|
1381
|
+
if src.is_file():
|
|
1382
|
+
d = json.loads(src.read_text())
|
|
1383
|
+
else:
|
|
1384
|
+
# 폴백: 최소 fixture 인라인
|
|
1385
|
+
d = {'meta': {}}
|
|
1386
|
+
d['meta']['reportLanguage'] = 'en'
|
|
1387
|
+
Path('$DATA').write_text(json.dumps(d))
|
|
1388
|
+
"
|
|
1389
|
+
|
|
1390
|
+
python3 "$REPO/scripts/okstra-render-final-report.py" "$DATA" --report-language en
|
|
1391
|
+
|
|
1392
|
+
OUT="$TMP/sample.md"
|
|
1393
|
+
test -f "$OUT" || { echo "FAIL: render did not produce $OUT"; exit 1; }
|
|
1394
|
+
|
|
1395
|
+
grep -q "Token Usage Summary" "$OUT" \
|
|
1396
|
+
|| { echo "FAIL: English heading missing"; head -50 "$OUT"; exit 1; }
|
|
1397
|
+
|
|
1398
|
+
if grep -q "토큰 사용량 요약" "$OUT"; then
|
|
1399
|
+
echo "FAIL: Korean heading present in English render"
|
|
1400
|
+
exit 1
|
|
1401
|
+
fi
|
|
1402
|
+
|
|
1403
|
+
echo "PASS: scenario-NN-final-report-en"
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
- [ ] **Step 3: ko 시나리오 작성**
|
|
1407
|
+
|
|
1408
|
+
`tests-e2e/scenario-MM-final-report-ko.sh`: 위와 동일하나 `reportLanguage: "ko"`, `grep -q "토큰 사용량 요약"`, 반대 가드 `if grep -q "Token Usage Summary"`.
|
|
1409
|
+
|
|
1410
|
+
- [ ] **Step 4: 두 시나리오 실행**
|
|
1411
|
+
|
|
1412
|
+
Run: `bash tests-e2e/scenario-NN-final-report-en.sh && bash tests-e2e/scenario-MM-final-report-ko.sh`
|
|
1413
|
+
Expected: 둘 다 `PASS: ...` 로 끝남.
|
|
1414
|
+
|
|
1415
|
+
- [ ] **Step 5: 커밋**
|
|
1416
|
+
|
|
1417
|
+
```bash
|
|
1418
|
+
git add tests-e2e/scenario-NN-final-report-en.sh tests-e2e/scenario-MM-final-report-ko.sh
|
|
1419
|
+
chmod +x tests-e2e/scenario-NN-final-report-en.sh tests-e2e/scenario-MM-final-report-ko.sh
|
|
1420
|
+
git commit -m "test(e2e): final-report renders en and ko separately"
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
---
|
|
1424
|
+
|
|
1425
|
+
## Task 14: CHANGES.md + runtime 빌드 + 최종 검수
|
|
1426
|
+
|
|
1427
|
+
**Files:**
|
|
1428
|
+
- Modify: `CHANGES.md`
|
|
1429
|
+
- Build: `runtime/` (via `npm run build`)
|
|
1430
|
+
|
|
1431
|
+
- [ ] **Step 1: `CHANGES.md` 에 항목 추가**
|
|
1432
|
+
|
|
1433
|
+
가장 위(최신) 에 다음을 삽입:
|
|
1434
|
+
|
|
1435
|
+
```markdown
|
|
1436
|
+
## YYYY-MM-DD final report language is now configurable
|
|
1437
|
+
|
|
1438
|
+
- 사용자 영향: 신규 `okstra setup` 의 final report 기본 언어가 영어로
|
|
1439
|
+
설정됩니다. 기존 프로젝트(`project.json.reportLanguage` 미설정) 는
|
|
1440
|
+
`auto` 로 동작해 task brief 의 주 서술 언어를 따라가므로, 한국어 brief
|
|
1441
|
+
를 쓰던 사용자는 별도 조치 없이 종전과 같이 한국어 보고서를 받습니다.
|
|
1442
|
+
- 새 CLI: `okstra config set report-language <en|ko|auto> --scope <project|global>`.
|
|
1443
|
+
- i18n 사전: `templates/reports/i18n/{en,ko}.json` 신설. 두 사전의 키셋은
|
|
1444
|
+
반드시 동일해야 하며, `tests/test_report_language_i18n.py` 가 회귀를 가드합니다.
|
|
1445
|
+
```
|
|
1446
|
+
|
|
1447
|
+
(`YYYY-MM-DD` 은 커밋 직전에 실제 날짜로 교체.)
|
|
1448
|
+
|
|
1449
|
+
- [ ] **Step 2: runtime/ 동기화**
|
|
1450
|
+
|
|
1451
|
+
Run: `npm run build`
|
|
1452
|
+
Expected: `runtime/` 가 새 사전 + 수정된 template + skills 로 갱신됨.
|
|
1453
|
+
|
|
1454
|
+
- [ ] **Step 3: 전체 테스트 통과 확인**
|
|
1455
|
+
|
|
1456
|
+
Run: `python3 -m pytest tests/ -v && bash validators/validate-workflow.sh`
|
|
1457
|
+
Expected: 모두 PASS.
|
|
1458
|
+
|
|
1459
|
+
- [ ] **Step 4: 모든 신규 e2e 시나리오 실행**
|
|
1460
|
+
|
|
1461
|
+
Run: `bash tests-e2e/scenario-NN-final-report-en.sh && bash tests-e2e/scenario-MM-final-report-ko.sh`
|
|
1462
|
+
Expected: 둘 다 PASS.
|
|
1463
|
+
|
|
1464
|
+
- [ ] **Step 5: CLI smoke**
|
|
1465
|
+
|
|
1466
|
+
Run: `node bin/okstra --version && node bin/okstra doctor && node bin/okstra config --help | grep -A2 report-language`
|
|
1467
|
+
Expected: 도움말에 `report-language` 키 설명이 보임.
|
|
1468
|
+
|
|
1469
|
+
- [ ] **Step 6: 한국어 잔존 0 최종 확인**
|
|
1470
|
+
|
|
1471
|
+
Run: `grep -P '[\x{AC00}-\x{D7A3}]' templates/reports/final-report.template.md`
|
|
1472
|
+
Expected: 출력 없음 (exit code 1).
|
|
1473
|
+
|
|
1474
|
+
- [ ] **Step 7: 커밋**
|
|
1475
|
+
|
|
1476
|
+
```bash
|
|
1477
|
+
git add CHANGES.md runtime/
|
|
1478
|
+
git commit -m "chore(release): bump CHANGES + rebuild runtime for report-language feature"
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
---
|
|
1482
|
+
|
|
1483
|
+
## 자체 점검 (자기 리뷰)
|
|
1484
|
+
|
|
1485
|
+
1. **Spec 커버리지** — spec §3 (데이터 모델), §4 (setup), §5 (template i18n), §6 (renderer), §7 (worker/lead 계약), §8 (테스트), §9 (마이그레이션), §10 (파일 목록) 모두 Task 1-14 에 대응. spec §11 의 미해결 4개 항목은 plan 머리말에서 확정. ✓
|
|
1486
|
+
2. **Placeholder 스캔** — "TBD", "TODO", "as needed", "etc." 단독 없음. `NN`/`MM` 시나리오 번호는 Task 13 Step 1 에서 명시적으로 확정. ✓
|
|
1487
|
+
3. **타입 일관성** — `render(data, *, template_path, report_language=None)` 시그니처는 Task 2/Task 11 에서 동일하게 사용. `resolve_report_language(data, *, override)` 도 일관. `KEYS["report-language"].jsonField === "reportLanguage"` 는 Task 5/Task 4/Task 12 모두 같은 카멜케이스. ✓
|
|
1488
|
+
4. **사전 키 충돌** — `sectionAside.tokenSummary` 와 `tokenSummary.heading` 이 둘 다 "Token Usage Summary" 를 표현. Task 9 에서 `tokenSummary.heading` 만 쓰고 `sectionAside.tokenSummary` 는 Task 8 에서 제거 또는 사용 안 함을 명시. → **수정 필요**: Task 8 의 `sectionAside` 키 목록에서 `tokenSummary` 제거.
|
|
1489
|
+
|
|
1490
|
+
(자기 리뷰에서 발견한 수정사항은 plan 본문에 inline 으로 반영.)
|
|
1491
|
+
|
|
1492
|
+
---
|
|
1493
|
+
|
|
1494
|
+
## 실행 옵션
|
|
1495
|
+
|
|
1496
|
+
계획서는 `docs/superpowers/plans/2026-05-20-final-report-language.md` 에 저장됨. 실행 방식 선택:
|
|
1497
|
+
|
|
1498
|
+
1. **Subagent-Driven (권장)** — task 마다 새 서브에이전트 dispatch, 사이에 review checkpoint. 빠른 반복.
|
|
1499
|
+
2. **Inline Execution** — 본 세션 안에서 task 를 순차 실행, batch 마다 checkpoint.
|
|
1500
|
+
|
|
1501
|
+
어느 쪽으로 진행할까요?
|