okstra 0.101.1 → 0.102.1
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 +5 -5
- package/README.md +5 -5
- package/docs/for-ai/README.md +2 -3
- package/docs/for-ai/skills/{okstra-container.md → okstra-container-build.md} +3 -4
- package/docs/for-ai/skills/okstra-inspect.md +1 -2
- package/docs/kr/architecture/storage-model.md +1 -2
- package/docs/kr/architecture.md +6 -6
- package/docs/kr/cli.md +3 -3
- package/docs/kr/container.md +1 -1
- package/docs/project-structure-overview.md +5 -5
- package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +1 -1
- package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +542 -0
- package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +568 -0
- package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +203 -0
- package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +525 -0
- package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +129 -0
- package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +185 -0
- package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +121 -0
- package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +110 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -0
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
- package/runtime/prompts/profiles/final-verification.md +4 -3
- package/runtime/prompts/profiles/implementation-planning.md +12 -9
- package/runtime/python/okstra_ctl/run.py +0 -1
- package/runtime/python/okstra_token_usage/paths.py +1 -2
- package/runtime/schemas/final-report-v1.0.schema.json +106 -3
- package/runtime/skills/{okstra-container → okstra-container-build}/SKILL.md +3 -3
- package/runtime/templates/reports/final-report.template.md +63 -2
- package/runtime/templates/reports/final-verification-input.template.md +3 -3
- package/runtime/templates/reports/i18n/en.json +28 -1
- package/runtime/templates/reports/i18n/ko.json +28 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +70 -1
- package/src/commands/lifecycle/install.mjs +17 -18
- package/src/commands/lifecycle/uninstall.mjs +8 -6
- package/src/lib/skill-catalog.mjs +10 -9
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# Decision Drafts 렌더 갭 해소 구현 계획
|
|
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:** implementation-planning 의 `Decision Drafts` 부록을 cross-project 와 동일하게 구조 필드(`decisionDrafts`/`skippedAdrCandidates`)로 승격해 결정론적 렌더 파이프라인에 실제로 출력시키고, line 116 의 materialization step MUST 를 validator 로 강제한다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** `data.json`(SSOT) → Jinja 템플릿 결정론 렌더. 스키마에 `$defs/DecisionDraft`·`$defs/SkippedAdrCandidate` 추가 → `implementationPlanning` 배열 2개 → 템플릿 §5.4 `### Decision Drafts` 블록 → i18n → validate-run 헤딩 contract + 자기완결 교차 필드 검사 → 프로파일/report-writer prose 정합 → 빌드 동기화. cross-project 직전 사례([2026-06-24-cross-project-precondition.md](2026-06-24-cross-project-precondition.md))와 동일 패턴.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** JSON Schema(Draft 2020-12 스타일, `okstra_ctl.final_report_schema`), Jinja2 템플릿, Python `validate-run.py`, pytest, Node `tools/build.mjs`.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- `runtime/` 직접 수정 금지 — 소스(`schemas/`, `templates/`, `prompts/`, `validators/`, `agents/`) 수정 후 `npm run build` 로 동기화 (CLAUDE.md).
|
|
14
|
+
- 완료 게이트: `npm run check` 가 그대로 exit 0 (test:js 실패 시 test:py 안 돔 — 통합 게이트로 확인).
|
|
15
|
+
- 동시 세션 환경 — `git commit --amend` 금지, 항상 새 커밋([project_concurrent_sessions_no_amend]). main 직접 커밋이 이 레포 관례.
|
|
16
|
+
- 사용자-대면 prose(스펙/플랜/프로파일 한국어 본문)는 한국어, 코드 식별자/경로/CLI 인자는 영어.
|
|
17
|
+
- i18n: `en.json` 과 `ko.json` 의 키는 **정확히 동일**해야 하며 같은 커밋에 추가([en.json:4](../../../templates/reports/i18n/en.json:4)).
|
|
18
|
+
- 헤딩 substring `Decision Drafts` 는 **세 곳**에 동시 존재해야 정합: 프로파일 line 57, `validate-run.py` `PLANNING_REQUIRED_SECTIONS`, 테스트 `_PLANNING_REQUIRED`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: 스키마 데이터 모델 + 픽스처
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Modify: `schemas/final-report-v1.0.schema.json` (`$defs` 2개 신설, `implementationPlanning` properties+required)
|
|
26
|
+
- Modify: `tests/fixtures/final-report-data/implementation-planning-001.data.json` (빈 배열 2개 추가)
|
|
27
|
+
- Create: `tests/contract/test_decision_drafts.py` (스키마 모양 테스트)
|
|
28
|
+
|
|
29
|
+
**Interfaces:**
|
|
30
|
+
- Produces: `$defs/DecisionDraft` (필드 `number`/`slug`/`status`/`context`/`decision`/`consequences`/`alternativesConsidered`), `$defs/SkippedAdrCandidate` (`topic`/`reason`), `implementationPlanning.decisionDrafts[]`, `implementationPlanning.skippedAdrCandidates[]`. Task 2(렌더)·Task 3(validator)·Task 4(prose)가 이 필드명을 그대로 참조.
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: 실패 테스트 작성** — `tests/contract/test_decision_drafts.py`
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
"""decisionDrafts / skippedAdrCandidates 스키마 모양 + validate-run 교차 필드 게이트 단위 테스트.
|
|
36
|
+
|
|
37
|
+
스키마는 각 draft 의 7필드 비-빈 + status enum 을 보장하고(모양),
|
|
38
|
+
validate-run 은 decisionDrafts 가 비어있지 않으면 어느 stage stepwise 가
|
|
39
|
+
`.okstra/decisions/` materialization step 을 포함하는지(존재)를 본다.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import copy
|
|
44
|
+
import importlib.util
|
|
45
|
+
import json
|
|
46
|
+
import sys
|
|
47
|
+
|
|
48
|
+
from _paths import REPO_ROOT
|
|
49
|
+
|
|
50
|
+
import pytest
|
|
51
|
+
|
|
52
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
53
|
+
|
|
54
|
+
from okstra_ctl.final_report_schema import load_schema, validate as schema_validate # noqa: E402
|
|
55
|
+
|
|
56
|
+
FIXTURE = (
|
|
57
|
+
REPO_ROOT / "tests" / "fixtures" / "final-report-data"
|
|
58
|
+
/ "implementation-planning-001.data.json"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
_SAMPLE_DRAFT = {
|
|
62
|
+
"number": "0007",
|
|
63
|
+
"slug": "classifier-table-naming",
|
|
64
|
+
"status": "Proposed",
|
|
65
|
+
"context": "classifier 스키마의 5개 테이블 명명 규칙을 정해야 한다.",
|
|
66
|
+
"decision": "snake_case + 도메인 접두어(classifier_) 를 사용한다.",
|
|
67
|
+
"consequences": "기존 마이그레이션과 일관되며 조인 가독성이 오른다.",
|
|
68
|
+
"alternativesConsidered": "camelCase(기각: 식별자 대소문자 이슈), 접두어 없음(기각: 충돌 위험).",
|
|
69
|
+
}
|
|
70
|
+
_SAMPLE_SKIPPED = {"topic": "ORM 도입 여부", "reason": "되돌리기 어렵지 않음 — 기준 ① 미달."}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.fixture(scope="module")
|
|
74
|
+
def schema():
|
|
75
|
+
return load_schema()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.fixture()
|
|
79
|
+
def data():
|
|
80
|
+
return json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_fixture_with_empty_decision_arrays_validates(schema, data):
|
|
84
|
+
assert schema_validate(data, schema) == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_valid_draft_and_skipped_validate(schema, data):
|
|
88
|
+
d = copy.deepcopy(data)
|
|
89
|
+
d["implementationPlanning"]["decisionDrafts"] = [copy.deepcopy(_SAMPLE_DRAFT)]
|
|
90
|
+
d["implementationPlanning"]["skippedAdrCandidates"] = [copy.deepcopy(_SAMPLE_SKIPPED)]
|
|
91
|
+
assert schema_validate(d, schema) == []
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_empty_context_is_rejected(schema, data):
|
|
95
|
+
d = copy.deepcopy(data)
|
|
96
|
+
bad = copy.deepcopy(_SAMPLE_DRAFT)
|
|
97
|
+
bad["context"] = ""
|
|
98
|
+
d["implementationPlanning"]["decisionDrafts"] = [bad]
|
|
99
|
+
assert schema_validate(d, schema) != []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_bad_slug_is_rejected(schema, data):
|
|
103
|
+
d = copy.deepcopy(data)
|
|
104
|
+
bad = copy.deepcopy(_SAMPLE_DRAFT)
|
|
105
|
+
bad["slug"] = "Classifier Table Naming" # 공백/대문자 — kebab 위반
|
|
106
|
+
d["implementationPlanning"]["decisionDrafts"] = [bad]
|
|
107
|
+
assert schema_validate(d, schema) != []
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_non_proposed_status_is_rejected(schema, data):
|
|
111
|
+
d = copy.deepcopy(data)
|
|
112
|
+
bad = copy.deepcopy(_SAMPLE_DRAFT)
|
|
113
|
+
bad["status"] = "Accepted"
|
|
114
|
+
d["implementationPlanning"]["decisionDrafts"] = [bad]
|
|
115
|
+
assert schema_validate(d, schema) != []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_short_number_is_rejected(schema, data):
|
|
119
|
+
d = copy.deepcopy(data)
|
|
120
|
+
bad = copy.deepcopy(_SAMPLE_DRAFT)
|
|
121
|
+
bad["number"] = "7" # zero-pad ≥4 위반
|
|
122
|
+
d["implementationPlanning"]["decisionDrafts"] = [bad]
|
|
123
|
+
assert schema_validate(d, schema) != []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_empty_skipped_topic_is_rejected(schema, data):
|
|
127
|
+
d = copy.deepcopy(data)
|
|
128
|
+
bad = copy.deepcopy(_SAMPLE_SKIPPED)
|
|
129
|
+
bad["topic"] = ""
|
|
130
|
+
d["implementationPlanning"]["skippedAdrCandidates"] = [bad]
|
|
131
|
+
assert schema_validate(d, schema) != []
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
135
|
+
|
|
136
|
+
Run: `python3 -m pytest tests/contract/test_decision_drafts.py -v`
|
|
137
|
+
Expected: FAIL — `test_valid_draft_and_skipped_validate` 등이 실패. 현재 `implementationPlanning` 은 `additionalProperties: false` 라 `decisionDrafts`/`skippedAdrCandidates` 키 자체가 거부되고, `test_fixture_with_empty_decision_arrays_validates` 도 픽스처에 키가 없어 (required 추가 전이므로) 통과하거나 무관 — 핵심은 inject 테스트가 FAIL 하는 것.
|
|
138
|
+
|
|
139
|
+
- [ ] **Step 3: 스키마에 `$defs` 2개 추가** — `schemas/final-report-v1.0.schema.json` `CrossProjectDependency` 정의([:1382](../../../schemas/final-report-v1.0.schema.json:1382)) **바로 뒤**(`ValidationCheckRow` 앞)에 삽입:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
"DecisionDraft": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"required": ["number", "slug", "status", "context", "decision", "consequences", "alternativesConsidered"],
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"properties": {
|
|
147
|
+
"number": { "type": "string", "pattern": "^\\d{4,}$" },
|
|
148
|
+
"slug": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
149
|
+
"status": { "enum": ["Proposed"] },
|
|
150
|
+
"context": { "type": "string", "minLength": 1 },
|
|
151
|
+
"decision": { "type": "string", "minLength": 1 },
|
|
152
|
+
"consequences": { "type": "string", "minLength": 1 },
|
|
153
|
+
"alternativesConsidered": { "type": "string", "minLength": 1 }
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
"SkippedAdrCandidate": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"required": ["topic", "reason"],
|
|
160
|
+
"additionalProperties": false,
|
|
161
|
+
"properties": {
|
|
162
|
+
"topic": { "type": "string", "minLength": 1 },
|
|
163
|
+
"reason": { "type": "string", "minLength": 1 }
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- [ ] **Step 4: `implementationPlanning` 에 properties + required 추가** — `schemas/final-report-v1.0.schema.json`
|
|
169
|
+
|
|
170
|
+
`required` 배열([:353-365](../../../schemas/final-report-v1.0.schema.json:353))의 `"crossProjectDependencies"` 다음에 두 항목 추가:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
"planBodyVerification",
|
|
174
|
+
"crossProjectDependencies",
|
|
175
|
+
"decisionDrafts",
|
|
176
|
+
"skippedAdrCandidates"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`properties` 의 `crossProjectDependencies` 블록([:415-418](../../../schemas/final-report-v1.0.schema.json:415)) 다음에 추가:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
"crossProjectDependencies": {
|
|
183
|
+
"type": "array",
|
|
184
|
+
"items": { "$ref": "#/$defs/CrossProjectDependency" }
|
|
185
|
+
},
|
|
186
|
+
"decisionDrafts": {
|
|
187
|
+
"type": "array",
|
|
188
|
+
"items": { "$ref": "#/$defs/DecisionDraft" }
|
|
189
|
+
},
|
|
190
|
+
"skippedAdrCandidates": {
|
|
191
|
+
"type": "array",
|
|
192
|
+
"items": { "$ref": "#/$defs/SkippedAdrCandidate" }
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- [ ] **Step 5: 픽스처에 빈 배열 2개 추가** — `tests/fixtures/final-report-data/implementation-planning-001.data.json`
|
|
197
|
+
|
|
198
|
+
`implementationPlanning.crossProjectDependencies` 배열 다음에 추가(required 충족, 가장 단순):
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
"crossProjectDependencies": [ ...기존 그대로... ],
|
|
202
|
+
"decisionDrafts": [],
|
|
203
|
+
"skippedAdrCandidates": []
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- [ ] **Step 6: 테스트 통과 확인 (신규 + 회귀)**
|
|
207
|
+
|
|
208
|
+
Run: `python3 -m pytest tests/contract/test_decision_drafts.py tests/report/test_schema_excerpt.py tests/contract/test_cross_project_precondition.py -v`
|
|
209
|
+
Expected: PASS — 신규 모양 테스트 7개 통과; `test_excerpt_validates_real_fixture_data`(픽스처가 full schema 검증)·`test_no_dangling_refs`(새 `$defs` 가 `$ref` 클로저로 자동 포함) 통과.
|
|
210
|
+
|
|
211
|
+
- [ ] **Step 7: 커밋**
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
git add schemas/final-report-v1.0.schema.json tests/fixtures/final-report-data/implementation-planning-001.data.json tests/contract/test_decision_drafts.py
|
|
215
|
+
git commit -m "feat(report-schema): add DecisionDraft + skippedAdrCandidates fields"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### Task 2: 템플릿 렌더 + i18n
|
|
221
|
+
|
|
222
|
+
**Files:**
|
|
223
|
+
- Modify: `templates/reports/final-report.template.md` (§5.4 에 `### Decision Drafts` 블록)
|
|
224
|
+
- Modify: `templates/reports/i18n/en.json`, `templates/reports/i18n/ko.json` (키 추가)
|
|
225
|
+
- Modify: `tests/report/test_render_final_report.py` (테스트 2개 추가 + `_PLANNING_REQUIRED` 갱신)
|
|
226
|
+
|
|
227
|
+
**Interfaces:**
|
|
228
|
+
- Consumes: Task 1 의 `implementationPlanning.decisionDrafts[]`(필드 number/slug/status/context/decision/consequences/alternativesConsidered), `skippedAdrCandidates[]`(topic/reason).
|
|
229
|
+
- Produces: 렌더된 `### Decision Drafts` 섹션 헤딩(substring) — Task 3 의 `PLANNING_REQUIRED_SECTIONS` 가 의존.
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 1: 실패 테스트 작성** — `tests/report/test_render_final_report.py` 끝부분(`test_cross_project_dependencies_empty_state` 인근)에 추가:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
_SAMPLE_DRAFT = {
|
|
235
|
+
"number": "0007",
|
|
236
|
+
"slug": "classifier-table-naming",
|
|
237
|
+
"status": "Proposed",
|
|
238
|
+
"context": "테이블 명명 규칙을 정해야 한다.",
|
|
239
|
+
"decision": "snake_case + classifier_ 접두어.",
|
|
240
|
+
"consequences": "조인 가독성이 오른다.",
|
|
241
|
+
"alternativesConsidered": "camelCase(기각), 접두어 없음(기각).",
|
|
242
|
+
}
|
|
243
|
+
_SAMPLE_SKIPPED = {"topic": "ORM 도입", "reason": "되돌리기 어렵지 않음 — 기준 ① 미달."}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_decision_drafts_render():
|
|
247
|
+
data = _load("implementation-planning-001.data.json")
|
|
248
|
+
data["implementationPlanning"]["decisionDrafts"] = [dict(_SAMPLE_DRAFT)]
|
|
249
|
+
data["implementationPlanning"]["skippedAdrCandidates"] = [dict(_SAMPLE_SKIPPED)]
|
|
250
|
+
rendered = render(data, template_path=find_default_template())
|
|
251
|
+
assert "### Decision Drafts" in rendered
|
|
252
|
+
assert "#### 0007-classifier-table-naming" in rendered
|
|
253
|
+
assert "Proposed" in rendered
|
|
254
|
+
assert "skipped adr-candidate: ORM 도입 — reason:" in rendered
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_decision_drafts_empty_state():
|
|
258
|
+
rendered = _render_fixture("implementation-planning-001.data.json") # 픽스처는 빈 배열
|
|
259
|
+
assert "### Decision Drafts" in rendered
|
|
260
|
+
section = rendered[
|
|
261
|
+
rendered.index("### Decision Drafts"):
|
|
262
|
+
rendered.index("## 5.5 Stage Map")
|
|
263
|
+
]
|
|
264
|
+
assert "####" not in section # draft 서브섹션 없음
|
|
265
|
+
assert "skipped adr-candidate:" not in section
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
269
|
+
|
|
270
|
+
Run: `python3 -m pytest tests/report/test_render_final_report.py::test_decision_drafts_render tests/report/test_render_final_report.py::test_decision_drafts_empty_state -v`
|
|
271
|
+
Expected: FAIL — 템플릿에 `### Decision Drafts` 블록이 없어 헤딩 부재 + `t()` 키 미정의.
|
|
272
|
+
|
|
273
|
+
- [ ] **Step 3: 템플릿 블록 추가** — `templates/reports/final-report.template.md` 의 `### Requirement Coverage` 표 끝([:258](../../../templates/reports/final-report.template.md:258))과 `## 5.5 Stage Map`([:260](../../../templates/reports/final-report.template.md:260)) **사이**에 삽입:
|
|
274
|
+
|
|
275
|
+
```jinja
|
|
276
|
+
### Decision Drafts{% if t("sectionAside.decisionDrafts") != "Decision Drafts" %} ({{ t("sectionAside.decisionDrafts") }}){% endif %}
|
|
277
|
+
|
|
278
|
+
{% if (implementationPlanning.decisionDrafts | length == 0) and (implementationPlanning.skippedAdrCandidates | length == 0) -%}
|
|
279
|
+
{{ t("emptyState.decisionDrafts") }}
|
|
280
|
+
{%- else %}
|
|
281
|
+
{% for d in implementationPlanning.decisionDrafts %}
|
|
282
|
+
#### {{ d.number }}-{{ d.slug }}
|
|
283
|
+
|
|
284
|
+
- **{{ t("implementationPlanning.decisionDraftFields.status") }}:** {{ d.status }}
|
|
285
|
+
- **{{ t("implementationPlanning.decisionDraftFields.context") }}:** {{ d.context }}
|
|
286
|
+
- **{{ t("implementationPlanning.decisionDraftFields.decision") }}:** {{ d.decision }}
|
|
287
|
+
- **{{ t("implementationPlanning.decisionDraftFields.consequences") }}:** {{ d.consequences }}
|
|
288
|
+
- **{{ t("implementationPlanning.decisionDraftFields.alternativesConsidered") }}:** {{ d.alternativesConsidered }}
|
|
289
|
+
{% endfor %}
|
|
290
|
+
{% for s in implementationPlanning.skippedAdrCandidates %}
|
|
291
|
+
- skipped adr-candidate: {{ s.topic }} — reason: {{ s.reason }}
|
|
292
|
+
{% endfor %}
|
|
293
|
+
{%- endif %}
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- [ ] **Step 4: i18n 키 추가 (en.json)** — `templates/reports/i18n/en.json`
|
|
298
|
+
|
|
299
|
+
`emptyState`([:6](../../../templates/reports/i18n/en.json:6))에 추가: `"decisionDrafts": "- No decision drafts or skipped ADR candidates."`
|
|
300
|
+
`sectionAside`([:32](../../../templates/reports/i18n/en.json:32))에 추가: `"decisionDrafts": "Decision Drafts"`
|
|
301
|
+
`implementationPlanning`([:91](../../../templates/reports/i18n/en.json:91))의 `crossProjectRecognitionNote` 다음에 추가:
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
"decisionDraftFields": {
|
|
305
|
+
"status": "Status",
|
|
306
|
+
"context": "Context",
|
|
307
|
+
"decision": "Decision",
|
|
308
|
+
"consequences": "Consequences",
|
|
309
|
+
"alternativesConsidered": "Alternatives Considered"
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- [ ] **Step 5: i18n 키 추가 (ko.json — en.json 과 동일 키)** — `templates/reports/i18n/ko.json`
|
|
314
|
+
|
|
315
|
+
`emptyState` 에 추가: `"decisionDrafts": "- 결정 기록 초안이나 스킵된 ADR 후보 없음."`
|
|
316
|
+
`sectionAside` 에 추가: `"decisionDrafts": "결정 기록 초안"`
|
|
317
|
+
`implementationPlanning` 의 `crossProjectRecognitionNote` 다음에 추가:
|
|
318
|
+
|
|
319
|
+
```json
|
|
320
|
+
"decisionDraftFields": {
|
|
321
|
+
"status": "상태",
|
|
322
|
+
"context": "맥락",
|
|
323
|
+
"decision": "결정",
|
|
324
|
+
"consequences": "결과",
|
|
325
|
+
"alternativesConsidered": "검토한 대안"
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- [ ] **Step 6: `_PLANNING_REQUIRED` 에 헤딩 추가** — `tests/report/test_render_final_report.py:43`
|
|
330
|
+
|
|
331
|
+
`_PLANNING_REQUIRED` 튜플([:43-56](../../../tests/report/test_render_final_report.py:43))의 `"Cross-Project Dependencies",` 다음에 `"Decision Drafts",` 추가.
|
|
332
|
+
|
|
333
|
+
- [ ] **Step 7: 렌더 테스트 통과 확인**
|
|
334
|
+
|
|
335
|
+
Run: `python3 -m pytest tests/report/test_render_final_report.py -v`
|
|
336
|
+
Expected: PASS — 신규 2개 + `test_implementation_planning_fixture_contains_all_required_substrings`(이제 `Decision Drafts` 포함, 빈 배열 픽스처가 empty-state 로 헤딩 렌더) 통과.
|
|
337
|
+
|
|
338
|
+
- [ ] **Step 8: 커밋**
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
git add templates/reports/final-report.template.md templates/reports/i18n/en.json templates/reports/i18n/ko.json tests/report/test_render_final_report.py
|
|
342
|
+
git commit -m "feat(report): render Decision Drafts section in implementation-planning"
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
### Task 3: validator 자기완결 trigger + 헤딩 contract
|
|
348
|
+
|
|
349
|
+
**Files:**
|
|
350
|
+
- Modify: `validators/validate-run.py` (`PLANNING_REQUIRED_SECTIONS` + 신규 함수 + dispatch)
|
|
351
|
+
- Modify: `tests/contract/test_decision_drafts.py` (validator 테스트 추가)
|
|
352
|
+
|
|
353
|
+
**Interfaces:**
|
|
354
|
+
- Consumes: Task 1 의 `decisionDrafts[]`, `implementationPlanning.stages[].stepwiseExecution[].{files,action}`.
|
|
355
|
+
- Produces: `_validate_implementation_planning_decision_drafts(data: dict, failures: list[str]) -> None`.
|
|
356
|
+
|
|
357
|
+
- [ ] **Step 1: validator 실패 테스트 추가** — `tests/contract/test_decision_drafts.py` 끝에 추가:
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
VALIDATOR_PATH = REPO_ROOT / "validators" / "validate-run.py"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _load_validator():
|
|
364
|
+
spec = importlib.util.spec_from_file_location("validate_run", VALIDATOR_PATH)
|
|
365
|
+
module = importlib.util.module_from_spec(spec)
|
|
366
|
+
assert spec.loader is not None
|
|
367
|
+
spec.loader.exec_module(module)
|
|
368
|
+
return module
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@pytest.fixture(scope="module")
|
|
372
|
+
def vr():
|
|
373
|
+
return _load_validator()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _planning_data(drafts, stages):
|
|
377
|
+
return {
|
|
378
|
+
"header": {"taskType": "implementation-planning"},
|
|
379
|
+
"implementationPlanning": {"decisionDrafts": drafts, "stages": stages},
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_drafts_without_materialization_step_fails(vr):
|
|
384
|
+
failures: list[str] = []
|
|
385
|
+
data = _planning_data(
|
|
386
|
+
[dict(_SAMPLE_DRAFT)],
|
|
387
|
+
[{"stepwiseExecution": [
|
|
388
|
+
{"step": 1, "action": "GREEN: add classifier table", "files": "migrations/x.sql"},
|
|
389
|
+
]}],
|
|
390
|
+
)
|
|
391
|
+
vr._validate_implementation_planning_decision_drafts(data, failures)
|
|
392
|
+
assert failures != []
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_drafts_with_materialization_step_passes(vr):
|
|
396
|
+
failures: list[str] = []
|
|
397
|
+
data = _planning_data(
|
|
398
|
+
[dict(_SAMPLE_DRAFT)],
|
|
399
|
+
[{"stepwiseExecution": [
|
|
400
|
+
{"step": 1, "action": "GREEN: add classifier table", "files": "migrations/x.sql"},
|
|
401
|
+
{"step": 2,
|
|
402
|
+
"action": "create decision record from §5.4 Decision Drafts 0007-classifier-table-naming",
|
|
403
|
+
"files": ".okstra/decisions/0007-classifier-table-naming.md"},
|
|
404
|
+
]}],
|
|
405
|
+
)
|
|
406
|
+
vr._validate_implementation_planning_decision_drafts(data, failures)
|
|
407
|
+
assert failures == []
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_empty_drafts_is_noop(vr):
|
|
411
|
+
failures: list[str] = []
|
|
412
|
+
data = _planning_data([], [{"stepwiseExecution": [{"step": 1, "action": "x", "files": "y"}]}])
|
|
413
|
+
vr._validate_implementation_planning_decision_drafts(data, failures)
|
|
414
|
+
assert failures == []
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
`_SAMPLE_DRAFT` 는 Task 1 에서 이미 이 파일 상단에 정의됨 — 재사용.
|
|
418
|
+
|
|
419
|
+
- [ ] **Step 2: 테스트 실패 확인**
|
|
420
|
+
|
|
421
|
+
Run: `python3 -m pytest tests/contract/test_decision_drafts.py -k "materialization or noop" -v`
|
|
422
|
+
Expected: FAIL — `AttributeError: module 'validate_run' has no attribute '_validate_implementation_planning_decision_drafts'`.
|
|
423
|
+
|
|
424
|
+
- [ ] **Step 3: validator 함수 추가** — `validators/validate-run.py` 의 `_validate_implementation_planning_cross_project` 함수([:1534-1562](../../../validators/validate-run.py:1534)) **바로 뒤**에 삽입:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
def _validate_implementation_planning_decision_drafts(data: dict, failures: list[str]) -> None:
|
|
428
|
+
"""`decisionDrafts` 가 비어있지 않으면 어느 stage 의 stepwiseExecution step 이
|
|
429
|
+
`.okstra/decisions/` 파일을 생성하는 materialization step 을 포함해야 한다.
|
|
430
|
+
프로파일 Decision-record evaluation 의 "approved plan stepwise MUST include
|
|
431
|
+
Create .okstra/decisions/<NNNN>-<slug>.md" 를 실제 강제한다. draft 존재 자체가
|
|
432
|
+
trigger 이므로 self-contained 하다.
|
|
433
|
+
"""
|
|
434
|
+
ip = data.get("implementationPlanning")
|
|
435
|
+
if not isinstance(ip, dict):
|
|
436
|
+
return
|
|
437
|
+
if not (ip.get("decisionDrafts") or []):
|
|
438
|
+
return
|
|
439
|
+
has_materialization = any(
|
|
440
|
+
".okstra/decisions/" in (step.get("files") or "")
|
|
441
|
+
or ".okstra/decisions/" in (step.get("action") or "")
|
|
442
|
+
for stage in (ip.get("stages") or [])
|
|
443
|
+
if isinstance(stage, dict)
|
|
444
|
+
for step in (stage.get("stepwiseExecution") or [])
|
|
445
|
+
if isinstance(step, dict)
|
|
446
|
+
)
|
|
447
|
+
if not has_materialization:
|
|
448
|
+
failures.append(
|
|
449
|
+
"final-report data.json: implementationPlanning.decisionDrafts is "
|
|
450
|
+
"non-empty but no stage's stepwiseExecution creates a "
|
|
451
|
+
"`.okstra/decisions/<NNNN>-<slug>.md` file. The approved plan must "
|
|
452
|
+
"materialize each decision draft via a stepwise step (profile "
|
|
453
|
+
"Decision-record evaluation)."
|
|
454
|
+
)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- [ ] **Step 4: dispatch 배선** — `validators/validate-run.py:1492-1493`
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
elif task_type == "implementation-planning":
|
|
461
|
+
_validate_implementation_planning_cross_project(data, failures)
|
|
462
|
+
_validate_implementation_planning_decision_drafts(data, failures)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
- [ ] **Step 5: 헤딩 contract 추가** — `validators/validate-run.py:1132`
|
|
466
|
+
|
|
467
|
+
`PLANNING_REQUIRED_SECTIONS` 튜플([:1132-1144](../../../validators/validate-run.py:1132))의 `"Cross-Project Dependencies",` 다음에 `"Decision Drafts",` 추가.
|
|
468
|
+
|
|
469
|
+
- [ ] **Step 6: validator 테스트 통과 확인**
|
|
470
|
+
|
|
471
|
+
Run: `python3 -m pytest tests/contract/test_decision_drafts.py -v`
|
|
472
|
+
Expected: PASS — 모양 7개 + validator 3개 모두 통과.
|
|
473
|
+
|
|
474
|
+
- [ ] **Step 7: 커밋**
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
git add validators/validate-run.py tests/contract/test_decision_drafts.py
|
|
478
|
+
git commit -m "feat(validate-run): gate decisionDrafts on materialization step + heading"
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
### Task 4: 프로파일 / report-writer prose 정합
|
|
484
|
+
|
|
485
|
+
**Files:**
|
|
486
|
+
- Modify: `prompts/profiles/implementation-planning.md` (heading contract line 57; Decision-record evaluation 111-116; self-review)
|
|
487
|
+
- Modify: `agents/workers/report-writer-worker.md` (populate 안내 1줄)
|
|
488
|
+
|
|
489
|
+
**Interfaces:**
|
|
490
|
+
- Consumes: Task 1-3 의 필드명·검증 규칙. prose 만 변경, 코드 무변경.
|
|
491
|
+
|
|
492
|
+
- [ ] **Step 1: heading contract 갱신** — `prompts/profiles/implementation-planning.md:57`
|
|
493
|
+
|
|
494
|
+
`Cross-Project Dependencies`, 다음(혹은 `Requirement Coverage` 인근)에 `Decision Drafts` 를 필수 substring 목록에 추가. 즉 `... \`Cross-Project Dependencies\`, \`Decision Drafts\`, \`Validation Checklist\`, ...` 형태.
|
|
495
|
+
|
|
496
|
+
- [ ] **Step 2: Decision-record evaluation 블록 재작성** — `prompts/profiles/implementation-planning.md:115-116`
|
|
497
|
+
|
|
498
|
+
기존 두 단락(line 115 "If **all three** hold, attach a decision draft as a report appendix section ..." + line 116 "The drafts are NOT written by this phase. ...")을 다음으로 교체:
|
|
499
|
+
|
|
500
|
+
```markdown
|
|
501
|
+
If **all three** hold, record a decision draft as a `decisionDrafts[]` row (rendered in §5.4 `### Decision Drafts`, one `#### <number>-<slug>` subsection each). Each row carries `number` (= `(max existing in <PROJECT_ROOT>/.okstra/decisions/ + 1)` zero-padded to ≥4 digits), `slug` (kebab-case), `status: Proposed`, and the `context` / `decision` / `consequences` / `alternativesConsidered` fields (`alternativesConsidered` names the rejected alternatives and why). If any of the three criteria is missing, do NOT raise a draft — instead record a `skippedAdrCandidates[]` row (`topic` + `reason: <criterion that failed>`, rendered as a `skipped adr-candidate: … — reason: …` line under the same section) so the next reader knows the candidate was evaluated and intentionally dropped.
|
|
502
|
+
The decision files are NOT written by this phase. The approved plan's stepwise execution order MUST include the step `Create <PROJECT_ROOT>/.okstra/decisions/<NNNN>-<slug>.md from the §5.4 Decision Drafts subsection <number>-<slug>` (materializing the structured fields into the file's `## Status / ## Context / ## Decision / ## Consequences / ## Alternatives Considered` shape) so the `implementation` run commits the file inside okstra's subtree. `validators/validate-run.py` enforces this: a non-empty `decisionDrafts` whose stages carry no stepwise step referencing `.okstra/decisions/` is `contract-violated`.
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- [ ] **Step 3: self-review 패스에 점검 추가** — `prompts/profiles/implementation-planning.md` self-review([:123-132](../../../prompts/profiles/implementation-planning.md:123))의 마지막 항목(9. Cross-project) 다음에 추가:
|
|
506
|
+
|
|
507
|
+
```markdown
|
|
508
|
+
10. **Decision-draft materialization check** — `decisionDrafts` 가 비어있지 않으면, 매칭 materialization step(`.okstra/decisions/<NNNN>-<slug>.md` 생성)이 어느 stage 의 stepwise 에 존재하는지, draft 개수와 materialization step 이 1:1 로 대응하는지 reviewer 로서 확인한다. validator 는 step 의 *존재*만 보므로 `<NNNN>-<slug>` 정확성·개수 대응은 self-review 가 책임진다.
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- [ ] **Step 4: report-writer populate 안내 추가** — `agents/workers/report-writer-worker.md`
|
|
512
|
+
|
|
513
|
+
implementation-planning requirementCoverage 안내([:103](../../../agents/workers/report-writer-worker.md:103)) 다음에 한 줄 추가:
|
|
514
|
+
|
|
515
|
+
```markdown
|
|
516
|
+
- For `implementation-planning`, also populate `implementationPlanning.decisionDrafts` (one row per decision meeting all three decision-record criteria; `[]` otherwise) and `implementationPlanning.skippedAdrCandidates` (evaluated-but-dropped adr-candidates; `[]` otherwise). The schema excerpt enumerates the row shape; the renderer emits §5.4 `### Decision Drafts`.
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
- [ ] **Step 5: phase contract validator 확인**
|
|
520
|
+
|
|
521
|
+
Run: `bash validators/validate-workflow.sh`
|
|
522
|
+
Expected: exit 0 — 프로파일/템플릿 헤딩 contract 정합.
|
|
523
|
+
|
|
524
|
+
- [ ] **Step 6: 커밋**
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
git add prompts/profiles/implementation-planning.md agents/workers/report-writer-worker.md
|
|
528
|
+
git commit -m "docs(profile): align Decision Drafts prose with structured fields"
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
### Task 5: 빌드 동기화 + 완료 게이트
|
|
534
|
+
|
|
535
|
+
**Files:** 없음(빌드 출력 `runtime/` 은 gitignored — 커밋 대상 아님)
|
|
536
|
+
|
|
537
|
+
- [ ] **Step 1: runtime 동기화**
|
|
538
|
+
|
|
539
|
+
Run: `npm run build`
|
|
540
|
+
Expected: exit 0 — 소스 6개 디렉터리가 `runtime/` 으로 rsync.
|
|
541
|
+
|
|
542
|
+
- [ ] **Step 2: 완료 게이트 — 전체 스위트**
|
|
543
|
+
|
|
544
|
+
Run: `npm run check`
|
|
545
|
+
Expected: exit 0 — test:js 후 test:py 전부 통과. 실패 시 해당 Task 로 돌아가 수정.
|
|
546
|
+
|
|
547
|
+
- [ ] **Step 3: 빌드 산출물 정합 스모크**
|
|
548
|
+
|
|
549
|
+
Run: `node bin/okstra doctor`
|
|
550
|
+
Expected: 런타임/스킬 진단 정상.
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Self-Review
|
|
555
|
+
|
|
556
|
+
**1. Spec coverage** (스펙 §3 각 항목 → Task):
|
|
557
|
+
- §3.1 데이터 모델(`$defs` 2개, properties+required) → Task 1 Step 3-4 ✓
|
|
558
|
+
- §3.2 렌더(템플릿 §5.4 블록) + i18n → Task 2 Step 3-5 ✓
|
|
559
|
+
- §3.3(a) 모양 강제(스키마) → Task 1 ✓ / §3.3(b) 자기완결 trigger → Task 3 Step 3-4 ✓ / §3.3(c) 헤딩 contract → Task 2 Step 6 + Task 3 Step 5 + Task 4 Step 1 (세 곳) ✓
|
|
560
|
+
- §3.4 프로파일 prose → Task 4 Step 1-3 ✓
|
|
561
|
+
- §3.5 report-writer 정합 → Task 4 Step 4 ✓
|
|
562
|
+
- §3.6 excerpt/빌드 → Task 1 Step 6(`test_no_dangling_refs` 자동 커버) + Task 5 Step 1 ✓
|
|
563
|
+
- §6 검증 계획(render/validator/excerpt/픽스처 + `npm run check`) → Task 1·2·3 테스트 + Task 5 ✓
|
|
564
|
+
- §7 한계(완전성·1:1 정확성 watertight 불가) → Task 4 Step 3 self-review prose 로 보강 ✓
|
|
565
|
+
|
|
566
|
+
**2. Placeholder scan:** 모든 step 에 실제 코드/명령/기대출력 명시. "TBD"/"적절히"/"유사하게" 없음. ✓
|
|
567
|
+
|
|
568
|
+
**3. Type consistency:** 필드명 `number`/`slug`/`status`/`context`/`decision`/`consequences`/`alternativesConsidered`(draft), `topic`/`reason`(skipped), 함수 `_validate_implementation_planning_decision_drafts`, i18n 키 `decisionDraftFields.{status,context,decision,consequences,alternativesConsidered}` — Task 1→2→3→4 전체에서 동일하게 사용. 헤딩 substring `Decision Drafts` 는 세 곳(테스트/validator/프로파일) 일치. ✓
|