okstra 0.38.1 → 0.40.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 +1 -1
- package/README.md +1 -1
- package/docs/kr/architecture.md +18 -2
- package/docs/kr/cli.md +1 -1
- package/docs/project-structure-overview.md +2 -3
- package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +326 -0
- package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +337 -0
- package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +410 -0
- package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +728 -0
- package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +113 -0
- package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +173 -0
- package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +154 -0
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +3 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/{python → bin}/lib/okstra/usage.sh +3 -2
- package/runtime/bin/okstra-codex-exec.sh +3 -3
- package/runtime/bin/okstra-trace-cleanup.sh +64 -26
- package/runtime/prompts/profiles/_common-contract.md +9 -5
- package/runtime/prompts/profiles/final-verification.md +18 -16
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/requirements-discovery.md +18 -1
- package/runtime/prompts/wizard/prompts.ko.json +11 -0
- package/runtime/python/okstra_ctl/consumers.py +1 -1
- package/runtime/python/okstra_ctl/fanout.py +35 -0
- package/runtime/python/okstra_ctl/migrate.py +21 -42
- package/runtime/python/okstra_ctl/reconcile.py +2 -2
- package/runtime/python/okstra_ctl/render_final_report.py +0 -1
- package/runtime/python/okstra_ctl/run.py +0 -29
- package/runtime/python/okstra_ctl/run_context.py +9 -12
- package/runtime/python/okstra_ctl/seeding.py +0 -192
- package/runtime/python/okstra_ctl/wizard.py +70 -5
- package/runtime/python/okstra_ctl/work_categories.py +21 -0
- package/runtime/python/okstra_ctl/worktree.py +74 -77
- package/runtime/python/okstra_project/__init__.py +0 -6
- package/runtime/python/okstra_project/dirs.py +0 -8
- package/runtime/schemas/final-report-v1.0.schema.json +34 -27
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +2 -0
- package/runtime/templates/prd/brief.template.md +1 -1
- package/runtime/templates/reports/fan-out-unit.template.md +25 -0
- package/runtime/templates/reports/final-report.template.md +24 -13
- package/runtime/templates/reports/final-verification-input.template.md +16 -5
- package/runtime/templates/reports/i18n/en.json +6 -3
- package/runtime/templates/reports/i18n/ko.json +6 -3
- package/runtime/templates/worker-prompt-preamble.md +7 -0
- package/runtime/validators/lib/fixtures.sh +2 -2
- package/runtime/validators/lib/validate-assets.sh +9 -0
- package/runtime/validators/validate-implementation-plan-stages.py +19 -11
- package/runtime/validators/validate-run.py +114 -0
- package/runtime/validators/validate-schedule.py +4 -4
- package/runtime/validators/validate_fanout.py +99 -0
- package/src/_proc.mjs +31 -0
- package/src/check-project.mjs +1 -25
- package/src/config.mjs +7 -31
- package/src/doctor.mjs +10 -29
- package/src/install.mjs +8 -36
- package/src/migrate.mjs +1 -18
- package/src/okstra-dirs.mjs +0 -11
- package/src/setup.mjs +1 -154
- package/src/uninstall.mjs +6 -13
- package/runtime/templates/okstra.CLAUDE.md +0 -104
- /package/runtime/{python → bin}/lib/okstra/cli.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/globals.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/interactive.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/project-resolver.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-batch.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-list.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-open.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-projects.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reconcile.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reindex.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-rerun.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-show.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-tail.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/main.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/prepare.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/usage.sh +0 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
# requirements-discovery 도메인 fan-out 구현 계획
|
|
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:** requirements-discovery 가 혼합 요청을 도메인별 packet 으로 분해해 fan-out 하고, 그 packet 들을 okstra-run 이 다시 소비하는 루프를 okstra-run 내부에서 완결한다.
|
|
6
|
+
|
|
7
|
+
**Architecture:** requirements-discovery 가 다항목/다도메인 요청을 N개 packet(`.okstra/tasks/<g>/<id>/runs/requirements-discovery/fan-out/unit-*.md`)으로 직접 생산한다. 각 packet 은 `--task-brief <경로>` 로 okstra-run 에 그대로 투입돼 새 task-key 가 된다. 검증은 improvement-discovery 패턴(SSOT 모듈 + 전용 validator + validate-run 훅)을 mirror 한다. okstra-brief 는 개입하지 않는다.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (pytest), 마크다운 템플릿/프로필, bash e2e.
|
|
10
|
+
|
|
11
|
+
설계 근거: [docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md](../specs/2026-06-02-requirements-discovery-fanout-design.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
- `scripts/okstra_ctl/work_categories.py` (신설) — 5-enum domain SSOT.
|
|
18
|
+
- `scripts/okstra_ctl/fanout.py` (신설) — depends-on DAG 위상정렬 + 순환검출.
|
|
19
|
+
- `validators/validate_fanout.py` (신설) — packet + index 계약 검증.
|
|
20
|
+
- `validators/validate-run.py` (수정) — requirements-discovery 일 때 fan-out 훅 호출.
|
|
21
|
+
- `templates/reports/fan-out-unit.template.md` (신설) — packet 템플릿.
|
|
22
|
+
- `prompts/profiles/requirements-discovery.md` (수정) — fan-out 계약 선언 + Non-goals.
|
|
23
|
+
- `prompts/profiles/requirements-discovery.md` / `skills/okstra-context-loader/SKILL.md` / `skills/okstra-inspect/SKILL.md` (수정) — `ops-change` → `ops` 교정.
|
|
24
|
+
- `tests/test_work_categories.py`, `tests/test_fanout.py`, `tests/test_validate_fanout.py` (신설).
|
|
25
|
+
- `tests-e2e/scenario-<id>-requirements-discovery-fanout.sh` (신설).
|
|
26
|
+
- `docs/kr/architecture.md`, `CHANGES.md` (수정).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Task 1: work-category(domain) SSOT 모듈
|
|
31
|
+
|
|
32
|
+
**Files:**
|
|
33
|
+
- Create: `scripts/okstra_ctl/work_categories.py`
|
|
34
|
+
- Test: `tests/test_work_categories.py`
|
|
35
|
+
|
|
36
|
+
- [ ] **Step 1: 실패하는 테스트 작성**
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# tests/test_work_categories.py
|
|
40
|
+
import sys
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
44
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
45
|
+
|
|
46
|
+
from okstra_ctl.work_categories import WORK_CATEGORIES, is_valid_category # noqa: E402
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_enum_is_exactly_five_canonical_categories():
|
|
50
|
+
assert WORK_CATEGORIES == ("bugfix", "feature", "refactor", "ops", "improvement")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_is_valid_category():
|
|
54
|
+
assert is_valid_category("bugfix")
|
|
55
|
+
assert not is_valid_category("ops-change") # 비표준 철자 거부
|
|
56
|
+
assert not is_valid_category("")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_ssot_no_ops_change_in_code_or_profile():
|
|
60
|
+
"""SSOT 도입 후 코드/프로필에 'ops-change' 가 남아있으면 fail (drift 가드)."""
|
|
61
|
+
targets = [
|
|
62
|
+
REPO_ROOT / "prompts" / "profiles" / "requirements-discovery.md",
|
|
63
|
+
REPO_ROOT / "scripts" / "okstra_ctl" / "work_categories.py",
|
|
64
|
+
]
|
|
65
|
+
for t in targets:
|
|
66
|
+
assert "ops-change" not in t.read_text(encoding="utf-8"), f"{t} still has ops-change"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- [ ] **Step 2: 실패 확인**
|
|
70
|
+
|
|
71
|
+
Run: `python3 -m pytest tests/test_work_categories.py -q`
|
|
72
|
+
Expected: FAIL — `ModuleNotFoundError: okstra_ctl.work_categories`
|
|
73
|
+
|
|
74
|
+
- [ ] **Step 3: 모듈 구현**
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# scripts/okstra_ctl/work_categories.py
|
|
78
|
+
"""requirements-discovery work-category(domain) SSOT.
|
|
79
|
+
|
|
80
|
+
profile / validators / wizard 는 이 모듈에서 enum 을 import 해야 한다. 다른 곳에서
|
|
81
|
+
재정의하면 single-reference-point 위반이며 tests/test_work_categories.py 가 거부한다.
|
|
82
|
+
|
|
83
|
+
다섯째 값은 `ops` 가 canonical 이다(`--work-category` CLI help 및 workflow.PHASE_RULES
|
|
84
|
+
와 일치). 구버전 문서 철자 `ops-change` 는 폐기된다.
|
|
85
|
+
"""
|
|
86
|
+
from __future__ import annotations
|
|
87
|
+
|
|
88
|
+
WORK_CATEGORIES: tuple[str, ...] = (
|
|
89
|
+
"bugfix",
|
|
90
|
+
"feature",
|
|
91
|
+
"refactor",
|
|
92
|
+
"ops",
|
|
93
|
+
"improvement",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_valid_category(value: str) -> bool:
|
|
98
|
+
return value in WORK_CATEGORIES
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- [ ] **Step 4: `ops-change` → `ops` 교정** (test_ssot_no_ops_change 통과시키기)
|
|
102
|
+
|
|
103
|
+
`prompts/profiles/requirements-discovery.md:17` 수정:
|
|
104
|
+
- 기존: ` - classify the work as bugfix, feature, improvement, refactor, or ops-change`
|
|
105
|
+
- 변경: ` - classify the work as bugfix, feature, improvement, refactor, or ops`
|
|
106
|
+
|
|
107
|
+
`skills/okstra-context-loader/SKILL.md:47` 및 `skills/okstra-inspect/SKILL.md:54` 의
|
|
108
|
+
`workCategory` 표 행에서 `ops-change` → `ops` 로 교정 (표시 문자열).
|
|
109
|
+
|
|
110
|
+
- [ ] **Step 5: 통과 확인**
|
|
111
|
+
|
|
112
|
+
Run: `python3 -m pytest tests/test_work_categories.py -q`
|
|
113
|
+
Expected: PASS (3 passed)
|
|
114
|
+
|
|
115
|
+
- [ ] **Step 6: 커밋**
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git add scripts/okstra_ctl/work_categories.py tests/test_work_categories.py \
|
|
119
|
+
prompts/profiles/requirements-discovery.md skills/okstra-context-loader/SKILL.md skills/okstra-inspect/SKILL.md
|
|
120
|
+
git commit -m "feat(work-categories): domain 5-enum SSOT + ops-change→ops 교정"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Task 2: depends-on DAG 위상정렬 + 순환검출 헬퍼
|
|
126
|
+
|
|
127
|
+
**Files:**
|
|
128
|
+
- Create: `scripts/okstra_ctl/fanout.py`
|
|
129
|
+
- Test: `tests/test_fanout.py`
|
|
130
|
+
|
|
131
|
+
- [ ] **Step 1: 실패하는 테스트 작성**
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# tests/test_fanout.py
|
|
135
|
+
import sys
|
|
136
|
+
from pathlib import Path
|
|
137
|
+
|
|
138
|
+
import pytest
|
|
139
|
+
|
|
140
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
141
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
142
|
+
|
|
143
|
+
from okstra_ctl.fanout import topological_order, CycleError # noqa: E402
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_linear_chain():
|
|
147
|
+
assert topological_order({"a": [], "b": ["a"], "c": ["b"]}) == ["a", "b", "c"]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_independent_units_are_id_sorted():
|
|
151
|
+
assert topological_order({"b": [], "a": []}) == ["a", "b"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_cycle_raises():
|
|
155
|
+
with pytest.raises(CycleError):
|
|
156
|
+
topological_order({"a": ["b"], "b": ["a"]})
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_unknown_dependency_raises():
|
|
160
|
+
with pytest.raises(ValueError):
|
|
161
|
+
topological_order({"a": ["nope"]})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- [ ] **Step 2: 실패 확인**
|
|
165
|
+
|
|
166
|
+
Run: `python3 -m pytest tests/test_fanout.py -q`
|
|
167
|
+
Expected: FAIL — `ModuleNotFoundError: okstra_ctl.fanout`
|
|
168
|
+
|
|
169
|
+
- [ ] **Step 3: 구현**
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# scripts/okstra_ctl/fanout.py
|
|
173
|
+
"""fan-out unit 의존 그래프: 위상정렬 + 순환검출 (Kahn, id 사전순 결정적)."""
|
|
174
|
+
from __future__ import annotations
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CycleError(ValueError):
|
|
178
|
+
"""depends-on 그래프에 순환이 있을 때."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def topological_order(units: dict[str, list[str]]) -> list[str]:
|
|
182
|
+
"""unit-id 를 착수 가능한 순서(의존 먼저)로 반환.
|
|
183
|
+
|
|
184
|
+
units: unit-id -> 그 unit 이 의존하는 unit-id 목록.
|
|
185
|
+
순환이면 CycleError, 알 수 없는 의존이면 ValueError.
|
|
186
|
+
"""
|
|
187
|
+
indeg = {u: 0 for u in units}
|
|
188
|
+
for u, deps in units.items():
|
|
189
|
+
for d in deps:
|
|
190
|
+
if d not in units:
|
|
191
|
+
raise ValueError(f"unit {u!r} depends on unknown unit {d!r}")
|
|
192
|
+
indeg[u] += 1
|
|
193
|
+
queue = sorted(u for u, n in indeg.items() if n == 0)
|
|
194
|
+
order: list[str] = []
|
|
195
|
+
while queue:
|
|
196
|
+
u = queue.pop(0)
|
|
197
|
+
order.append(u)
|
|
198
|
+
for v, deps in units.items():
|
|
199
|
+
if u in deps:
|
|
200
|
+
indeg[v] -= 1
|
|
201
|
+
if indeg[v] == 0:
|
|
202
|
+
queue.append(v)
|
|
203
|
+
queue.sort()
|
|
204
|
+
if len(order) != len(units):
|
|
205
|
+
remaining = sorted(set(units) - set(order))
|
|
206
|
+
raise CycleError(f"dependency cycle among units: {remaining}")
|
|
207
|
+
return order
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- [ ] **Step 4: 통과 확인**
|
|
211
|
+
|
|
212
|
+
Run: `python3 -m pytest tests/test_fanout.py -q`
|
|
213
|
+
Expected: PASS (4 passed)
|
|
214
|
+
|
|
215
|
+
- [ ] **Step 5: 커밋**
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
git add scripts/okstra_ctl/fanout.py tests/test_fanout.py
|
|
219
|
+
git commit -m "feat(fanout): depends-on DAG 위상정렬 + 순환검출 헬퍼"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Task 3: packet 템플릿 + validate_fanout 검증기
|
|
225
|
+
|
|
226
|
+
packet/index 포맷은 **validate_fanout 테스트가 계약의 SSOT** 다. 템플릿은 그 포맷을 따른다.
|
|
227
|
+
|
|
228
|
+
**Files:**
|
|
229
|
+
- Create: `templates/reports/fan-out-unit.template.md`
|
|
230
|
+
- Create: `validators/validate_fanout.py`
|
|
231
|
+
- Test: `tests/test_validate_fanout.py`
|
|
232
|
+
|
|
233
|
+
packet frontmatter 계약 (inline flow list 로 파싱 단순화):
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
---
|
|
237
|
+
unit-id: unit-001 # 파일명 stem 과 동일
|
|
238
|
+
domain: bugfix # WORK_CATEGORIES 중 하나
|
|
239
|
+
depends-on: [unit-002] # 같은 fan-out 내 unit-id 들의 inline 리스트 ([] 가능)
|
|
240
|
+
recommended-next-phase: error-analysis # error-analysis | implementation-planning
|
|
241
|
+
---
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- [ ] **Step 1: 실패하는 테스트 작성**
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
# tests/test_validate_fanout.py
|
|
248
|
+
import sys
|
|
249
|
+
from pathlib import Path
|
|
250
|
+
|
|
251
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
252
|
+
sys.path.insert(0, str(REPO_ROOT / "validators"))
|
|
253
|
+
|
|
254
|
+
from validate_fanout import validate_fanout # noqa: E402
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _write_unit(d: Path, uid: str, domain: str, deps: str, nxt: str = "error-analysis"):
|
|
258
|
+
(d / f"{uid}.md").write_text(
|
|
259
|
+
f"---\nunit-id: {uid}\ndomain: {domain}\ndepends-on: {deps}\n"
|
|
260
|
+
f"recommended-next-phase: {nxt}\n---\n\n# {uid}\n본문\n",
|
|
261
|
+
encoding="utf-8",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _write_index(d: Path, order: list[str]):
|
|
266
|
+
lines = ["# Fan-out Index", "> generated — do not hand-edit", ""]
|
|
267
|
+
lines += [f"{i+1}. {uid}" for i, uid in enumerate(order)]
|
|
268
|
+
(d / "index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_valid_fanout_passes(tmp_path: Path):
|
|
272
|
+
fo = tmp_path / "fan-out"; fo.mkdir()
|
|
273
|
+
_write_unit(fo, "unit-001", "bugfix", "[]")
|
|
274
|
+
_write_unit(fo, "unit-002", "feature", "[unit-001]")
|
|
275
|
+
_write_index(fo, ["unit-001", "unit-002"])
|
|
276
|
+
res = validate_fanout(tmp_path)
|
|
277
|
+
assert res.ok, res.errors
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_bad_domain_fails(tmp_path: Path):
|
|
281
|
+
fo = tmp_path / "fan-out"; fo.mkdir()
|
|
282
|
+
_write_unit(fo, "unit-001", "ops-change", "[]")
|
|
283
|
+
_write_index(fo, ["unit-001"])
|
|
284
|
+
res = validate_fanout(tmp_path)
|
|
285
|
+
assert not res.ok
|
|
286
|
+
assert any("domain" in e for e in res.errors)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_cycle_fails(tmp_path: Path):
|
|
290
|
+
fo = tmp_path / "fan-out"; fo.mkdir()
|
|
291
|
+
_write_unit(fo, "unit-001", "bugfix", "[unit-002]")
|
|
292
|
+
_write_unit(fo, "unit-002", "feature", "[unit-001]")
|
|
293
|
+
_write_index(fo, ["unit-001", "unit-002"])
|
|
294
|
+
res = validate_fanout(tmp_path)
|
|
295
|
+
assert not res.ok
|
|
296
|
+
assert any("cycle" in e.lower() for e in res.errors)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_index_mismatch_fails(tmp_path: Path):
|
|
300
|
+
fo = tmp_path / "fan-out"; fo.mkdir()
|
|
301
|
+
_write_unit(fo, "unit-001", "bugfix", "[]")
|
|
302
|
+
_write_index(fo, ["unit-001", "unit-999"]) # 존재하지 않는 unit 나열
|
|
303
|
+
res = validate_fanout(tmp_path)
|
|
304
|
+
assert not res.ok
|
|
305
|
+
assert any("index" in e.lower() for e in res.errors)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_no_fanout_dir_is_ok(tmp_path: Path):
|
|
309
|
+
# fan-out 안 한 단일 요청: 디렉토리 없으면 통과(검증 대상 아님).
|
|
310
|
+
res = validate_fanout(tmp_path)
|
|
311
|
+
assert res.ok
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- [ ] **Step 2: 실패 확인**
|
|
315
|
+
|
|
316
|
+
Run: `python3 -m pytest tests/test_validate_fanout.py -q`
|
|
317
|
+
Expected: FAIL — `ModuleNotFoundError: validate_fanout`
|
|
318
|
+
|
|
319
|
+
- [ ] **Step 3: validate_fanout.py 구현**
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
# validators/validate_fanout.py
|
|
323
|
+
"""Validator for requirements-discovery fan-out packets + index.
|
|
324
|
+
|
|
325
|
+
Called by validators/validate-run.py when task_type == "requirements-discovery"
|
|
326
|
+
and a `fan-out/` directory exists under the run dir.
|
|
327
|
+
"""
|
|
328
|
+
from __future__ import annotations
|
|
329
|
+
|
|
330
|
+
import re
|
|
331
|
+
import sys
|
|
332
|
+
from dataclasses import dataclass, field
|
|
333
|
+
from pathlib import Path
|
|
334
|
+
|
|
335
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
|
336
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
337
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
338
|
+
|
|
339
|
+
from okstra_ctl.work_categories import WORK_CATEGORIES # noqa: E402
|
|
340
|
+
from okstra_ctl.fanout import topological_order, CycleError # noqa: E402
|
|
341
|
+
|
|
342
|
+
_NEXT_PHASES = ("error-analysis", "implementation-planning")
|
|
343
|
+
_UNIT_RE = re.compile(r"unit-\d{3}")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@dataclass
|
|
347
|
+
class ValidationResult:
|
|
348
|
+
ok: bool
|
|
349
|
+
errors: list[str] = field(default_factory=list)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _frontmatter(text: str) -> dict[str, str]:
|
|
353
|
+
if not text.startswith("---"):
|
|
354
|
+
return {}
|
|
355
|
+
end = text.find("\n---", 3)
|
|
356
|
+
if end == -1:
|
|
357
|
+
return {}
|
|
358
|
+
fm: dict[str, str] = {}
|
|
359
|
+
for line in text[3:end].splitlines():
|
|
360
|
+
if ":" in line:
|
|
361
|
+
k, _, v = line.partition(":")
|
|
362
|
+
fm[k.strip()] = v.strip()
|
|
363
|
+
return fm
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _parse_deps(raw: str) -> list[str]:
|
|
367
|
+
return _UNIT_RE.findall(raw or "")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def validate_fanout(run_dir: Path) -> ValidationResult:
|
|
371
|
+
run_dir = Path(run_dir)
|
|
372
|
+
fo = run_dir / "fan-out"
|
|
373
|
+
if not fo.is_dir():
|
|
374
|
+
return ValidationResult(ok=True)
|
|
375
|
+
|
|
376
|
+
errors: list[str] = []
|
|
377
|
+
units: dict[str, list[str]] = {}
|
|
378
|
+
for pkt in sorted(fo.glob("unit-*.md")):
|
|
379
|
+
fm = _frontmatter(pkt.read_text(encoding="utf-8"))
|
|
380
|
+
uid = fm.get("unit-id", "")
|
|
381
|
+
if uid != pkt.stem:
|
|
382
|
+
errors.append(f"{pkt.name}: unit-id {uid!r} != filename stem {pkt.stem!r}")
|
|
383
|
+
if fm.get("domain") not in WORK_CATEGORIES:
|
|
384
|
+
errors.append(f"{pkt.name}: domain {fm.get('domain')!r} not in {WORK_CATEGORIES}")
|
|
385
|
+
if fm.get("recommended-next-phase") not in _NEXT_PHASES:
|
|
386
|
+
errors.append(f"{pkt.name}: recommended-next-phase not in {_NEXT_PHASES}")
|
|
387
|
+
units[pkt.stem] = _parse_deps(fm.get("depends-on", ""))
|
|
388
|
+
|
|
389
|
+
if not units:
|
|
390
|
+
errors.append("fan-out/: directory exists but no unit-*.md packets found")
|
|
391
|
+
return ValidationResult(ok=False, errors=errors)
|
|
392
|
+
|
|
393
|
+
order: list[str] = []
|
|
394
|
+
try:
|
|
395
|
+
order = topological_order(units)
|
|
396
|
+
except CycleError as exc:
|
|
397
|
+
errors.append(f"depends-on cycle: {exc}")
|
|
398
|
+
except ValueError as exc:
|
|
399
|
+
errors.append(f"depends-on unresolved: {exc}")
|
|
400
|
+
|
|
401
|
+
index = fo / "index.md"
|
|
402
|
+
if not index.is_file():
|
|
403
|
+
errors.append("fan-out/index.md missing")
|
|
404
|
+
else:
|
|
405
|
+
listed = _UNIT_RE.findall(index.read_text(encoding="utf-8"))
|
|
406
|
+
if set(listed) != set(units):
|
|
407
|
+
errors.append(
|
|
408
|
+
f"index.md units {sorted(set(listed))} != packets {sorted(units)}"
|
|
409
|
+
)
|
|
410
|
+
elif order and listed != order:
|
|
411
|
+
errors.append(f"index.md order {listed} is not the topological order {order}")
|
|
412
|
+
|
|
413
|
+
return ValidationResult(ok=not errors, errors=errors)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- [ ] **Step 4: 통과 확인**
|
|
417
|
+
|
|
418
|
+
Run: `python3 -m pytest tests/test_validate_fanout.py -q`
|
|
419
|
+
Expected: PASS (5 passed)
|
|
420
|
+
|
|
421
|
+
- [ ] **Step 5: packet 템플릿 작성**
|
|
422
|
+
|
|
423
|
+
```markdown
|
|
424
|
+
<!-- templates/reports/fan-out-unit.template.md -->
|
|
425
|
+
---
|
|
426
|
+
unit-id: {{UNIT_ID}}
|
|
427
|
+
domain: {{DOMAIN}}
|
|
428
|
+
depends-on: {{DEPENDS_ON}}
|
|
429
|
+
recommended-next-phase: {{NEXT_PHASE}}
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
# Fan-out Unit: {{UNIT_ID}} ({{DOMAIN}})
|
|
433
|
+
|
|
434
|
+
> requirements-discovery fan-out 산출 packet. `okstra-run --task-brief <이 파일 경로>`
|
|
435
|
+
> 로 새 task-key 를 시작한다. 이 파일은 그 run 의 입력 packet 이다.
|
|
436
|
+
|
|
437
|
+
## Scope
|
|
438
|
+
|
|
439
|
+
<!-- 이 단위가 다루는 작업 항목 1개를 자족적으로 서술. 다른 단위와 섞지 말 것. -->
|
|
440
|
+
|
|
441
|
+
## Evidence
|
|
442
|
+
|
|
443
|
+
<!-- path:line 근거. requirements-discovery 가 file inspection 으로 확인한 위치. -->
|
|
444
|
+
|
|
445
|
+
## Depends-on rationale
|
|
446
|
+
|
|
447
|
+
<!-- depends-on 에 적은 각 unit 에 왜 의존하는지 1줄씩. 없으면 _(none)_ -->
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
- [ ] **Step 6: 커밋**
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
git add validators/validate_fanout.py tests/test_validate_fanout.py templates/reports/fan-out-unit.template.md
|
|
454
|
+
git commit -m "feat(fanout): packet 템플릿 + validate_fanout 검증기"
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Task 4: validate-run.py 에 fan-out 훅 연결
|
|
460
|
+
|
|
461
|
+
**Files:**
|
|
462
|
+
- Modify: `validators/validate-run.py` (improvement 디스패치부 `_validate_improvement_discovery` ~line 1254, 호출부 ~line 1628 참고)
|
|
463
|
+
- Test: `tests/test_validate_fanout.py` (훅 호출 통합 테스트 1개 추가)
|
|
464
|
+
|
|
465
|
+
- [ ] **Step 1: 통합 테스트 추가 (실패)**
|
|
466
|
+
|
|
467
|
+
`tests/test_validate_fanout.py` 끝에 추가:
|
|
468
|
+
|
|
469
|
+
```python
|
|
470
|
+
def test_validate_run_invokes_fanout_hook(tmp_path: Path):
|
|
471
|
+
"""validate-run 의 디스패치 함수가 requirements-discovery 에서 fan-out 을 검증한다."""
|
|
472
|
+
sys.path.insert(0, str(REPO_ROOT / "validators"))
|
|
473
|
+
import importlib.util
|
|
474
|
+
spec = importlib.util.spec_from_file_location(
|
|
475
|
+
"validate_run_mod", REPO_ROOT / "validators" / "validate-run.py"
|
|
476
|
+
)
|
|
477
|
+
mod = importlib.util.module_from_spec(spec)
|
|
478
|
+
spec.loader.exec_module(mod)
|
|
479
|
+
|
|
480
|
+
run_dir = tmp_path / "runs" / "requirements-discovery"
|
|
481
|
+
fo = run_dir / "fan-out"; fo.mkdir(parents=True)
|
|
482
|
+
_write_unit(fo, "unit-001", "ops-change", "[]") # 잘못된 domain
|
|
483
|
+
_write_index(fo, ["unit-001"])
|
|
484
|
+
|
|
485
|
+
failures: list[str] = []
|
|
486
|
+
mod._validate_requirements_discovery_fanout(run_dir, failures)
|
|
487
|
+
assert any("requirements-discovery" in f and "domain" in f for f in failures)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
- [ ] **Step 2: 실패 확인**
|
|
491
|
+
|
|
492
|
+
Run: `python3 -m pytest tests/test_validate_fanout.py::test_validate_run_invokes_fanout_hook -q`
|
|
493
|
+
Expected: FAIL — `AttributeError: module has no attribute '_validate_requirements_discovery_fanout'`
|
|
494
|
+
|
|
495
|
+
- [ ] **Step 3: 디스패치 함수 추가**
|
|
496
|
+
|
|
497
|
+
`validators/validate-run.py` 의 `_validate_improvement_discovery` 함수 바로 아래에 추가
|
|
498
|
+
(스타일은 sibling 그대로):
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
|
|
502
|
+
"""requirements-discovery run 에 fan-out/ 이 있으면 packet+index 를 검증해
|
|
503
|
+
실패를 ``requirements-discovery: `` 접두로 folding 한다. fan-out 이 없으면 no-op.
|
|
504
|
+
"""
|
|
505
|
+
from pathlib import Path as _Path
|
|
506
|
+
if not (_Path(run_dir) / "fan-out").is_dir():
|
|
507
|
+
return
|
|
508
|
+
try:
|
|
509
|
+
from validate_fanout import validate_fanout # noqa: E402
|
|
510
|
+
except Exception as exc: # pragma: no cover - import guard
|
|
511
|
+
failures.append(
|
|
512
|
+
f"requirements-discovery: validate_fanout import failed — {exc}"
|
|
513
|
+
)
|
|
514
|
+
return
|
|
515
|
+
result = validate_fanout(_Path(run_dir))
|
|
516
|
+
if not result.ok:
|
|
517
|
+
for err in result.errors:
|
|
518
|
+
failures.append(f"requirements-discovery: {err}")
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- [ ] **Step 4: 호출부 연결**
|
|
522
|
+
|
|
523
|
+
`_validate_improvement_discovery(...)` 호출 라인(현재 1636) 바로 다음, `validate_report_views(report_path, failures)`(현재 1637) 앞에 추가. `run_dir` 은 improvement 분기와 동일하게 `report_path.parent.parent`(= `runs/<task-type>/`)로 계산한다:
|
|
524
|
+
|
|
525
|
+
```python
|
|
526
|
+
if task_type == "requirements-discovery":
|
|
527
|
+
run_dir = report_path.parent.parent
|
|
528
|
+
_validate_requirements_discovery_fanout(run_dir, failures)
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
(`report_path` 는 `runs/requirements-discovery/reports/final-report-*.md` 이므로 `parent.parent` 가 `runs/requirements-discovery/`, 그 아래 `fan-out/` 을 검증한다.)
|
|
532
|
+
|
|
533
|
+
- [ ] **Step 5: 통과 확인 + 전체 validator 테스트**
|
|
534
|
+
|
|
535
|
+
Run: `python3 -m pytest tests/test_validate_fanout.py -q`
|
|
536
|
+
Expected: PASS (6 passed)
|
|
537
|
+
|
|
538
|
+
- [ ] **Step 6: 커밋**
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
git add validators/validate-run.py tests/test_validate_fanout.py
|
|
542
|
+
git commit -m "feat(validate-run): requirements-discovery fan-out 검증 훅 연결"
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Task 5: requirements-discovery 프로필에 fan-out 계약 선언
|
|
548
|
+
|
|
549
|
+
**Files:**
|
|
550
|
+
- Modify: `prompts/profiles/requirements-discovery.md`
|
|
551
|
+
|
|
552
|
+
- [ ] **Step 1: fan-out 책임 섹션 추가**
|
|
553
|
+
|
|
554
|
+
`- Primary focus areas:` 블록 다음에 새 항목 추가:
|
|
555
|
+
|
|
556
|
+
```markdown
|
|
557
|
+
- Fan-out (multi-item / multi-domain requests only):
|
|
558
|
+
- 단일 항목/단일 도메인 요청은 fan-out 하지 않는다(현행 단일 라우팅 보존).
|
|
559
|
+
- 요청이 2개 이상 도메인에 걸치거나 독립 착수 가능한 작업 항목이 2개 이상이면,
|
|
560
|
+
각 항목을 `runs/requirements-discovery/fan-out/unit-<NNN>.md` packet 으로 발행한다.
|
|
561
|
+
packet 은 `templates/reports/fan-out-unit.template.md` 형식을 따르며 frontmatter
|
|
562
|
+
`domain`(work-category 5-enum), `depends-on`(같은 fan-out 내 unit-id 의 inline 리스트),
|
|
563
|
+
`recommended-next-phase`(error-analysis | implementation-planning)를 채운다.
|
|
564
|
+
- `runs/requirements-discovery/fan-out/index.md` 에 packet 을 depends-on 위상순서로
|
|
565
|
+
나열한다(생성 뷰; 직접 편집 금지 명시). depends-on 그래프는 DAG 여야 한다 — 순환이면
|
|
566
|
+
리포트에 경고하고 해당 의존을 끊는다.
|
|
567
|
+
- 최종 리포트에는 분해 결과를 중복하지 말고 "fan-out: N packets → fan-out/index.md" 한 줄
|
|
568
|
+
포인터만 둔다. packet 실행은 별도다: 사용자가 `okstra-run --task-brief <packet 경로>` 로
|
|
569
|
+
각 단위를 새 task-key 로 시작한다(이 phase 는 다운스트림 run 을 직접 시작하지 않는다).
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- [ ] **Step 2: Non-goals 갱신**
|
|
573
|
+
|
|
574
|
+
`- Non-goals:` 블록의 첫 항목 아래에 in-scope 명시 1줄 추가:
|
|
575
|
+
|
|
576
|
+
```markdown
|
|
577
|
+
- 작업 단위 분해(fan-out)는 이 phase 의 in-scope 다 — 단, 각 단위의 *해법 설계*·소스
|
|
578
|
+
편집·plan 작성은 여전히 non-goal 이며 다운스트림 phase 가 담당한다
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
- [ ] **Step 3: 빌드로 runtime 동기화 + 워크플로 validator**
|
|
582
|
+
|
|
583
|
+
Run: `npm run build && bash validators/validate-workflow.sh`
|
|
584
|
+
Expected: build 0/오류 없음, validator PASS
|
|
585
|
+
|
|
586
|
+
- [ ] **Step 4: 커밋**
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
git add prompts/profiles/requirements-discovery.md
|
|
590
|
+
git commit -m "feat(requirements-discovery): fan-out 분해 계약 선언 + Non-goals 갱신"
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Task 6: 문서 (architecture.md, CHANGES.md)
|
|
596
|
+
|
|
597
|
+
**Files:**
|
|
598
|
+
- Modify: `docs/kr/architecture.md`
|
|
599
|
+
- Modify: `CHANGES.md`
|
|
600
|
+
|
|
601
|
+
- [ ] **Step 1: architecture.md 에 fan-out 흐름 추가**
|
|
602
|
+
|
|
603
|
+
requirements-discovery 를 다루는 절(또는 phase 흐름 표 인근)에 추가:
|
|
604
|
+
|
|
605
|
+
```markdown
|
|
606
|
+
#### requirements-discovery fan-out
|
|
607
|
+
|
|
608
|
+
혼합/다항목 요청은 requirements-discovery 가 도메인(work-category 5-enum)별 packet 으로
|
|
609
|
+
분해해 `runs/requirements-discovery/fan-out/unit-*.md` 에 발행한다. 각 packet 은
|
|
610
|
+
`okstra-run --task-brief <경로>` 로 새 task-key 가 된다. 순서는 `index.md` 의 depends-on
|
|
611
|
+
위상정렬이 담고, task 화 이후의 통합 일정은 okstra-schedule 이 맡는다. okstra-brief 는
|
|
612
|
+
이 경로에 개입하지 않는다. 검증: `validators/validate_fanout.py`(validate-run 훅).
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
- [ ] **Step 2: CHANGES.md 항목 추가**
|
|
616
|
+
|
|
617
|
+
`## 2026-06-02` 섹션에 추가(없으면 상단에 신설):
|
|
618
|
+
|
|
619
|
+
```markdown
|
|
620
|
+
### feat(requirements-discovery): 혼합 요청 도메인 fan-out
|
|
621
|
+
|
|
622
|
+
- requirements-discovery 가 버그/개선/구현이 섞인 요청을 work-category(5-enum)별 packet 으로
|
|
623
|
+
분해해 `runs/requirements-discovery/fan-out/` 에 발행하고, 각 packet 을
|
|
624
|
+
`okstra-run --task-brief <경로>` 로 독립 task-key 로 fan-out 할 수 있게 함. 순서는 packet 의
|
|
625
|
+
`depends-on` 위상정렬(`index.md`)이 담는다. 단일 요청은 현행 단일 라우팅 그대로.
|
|
626
|
+
- work-category 다섯째 값을 `ops` 로 SSOT 화하고 일부 문서의 `ops-change` 철자를 교정.
|
|
627
|
+
- 사용자 영향: 다음 release + `npx -y okstra@latest install` 후 적용. 혼합 요청 시
|
|
628
|
+
requirements-discovery 가 packet 들을 만들어 주며, 각각을 `okstra-run` 으로 따로 시작하면 된다.
|
|
629
|
+
단일 요청 동작은 변화 없음.
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
- [ ] **Step 3: 커밋**
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
git add docs/kr/architecture.md CHANGES.md
|
|
636
|
+
git commit -m "docs(fanout): architecture + CHANGES 항목"
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Task 7: e2e 시나리오
|
|
642
|
+
|
|
643
|
+
**Files:**
|
|
644
|
+
- Create: `tests-e2e/scenario-<NN>-requirements-discovery-fanout.sh` (`ls tests-e2e/` 로 다음 번호 확인)
|
|
645
|
+
|
|
646
|
+
- [ ] **Step 1: 시나리오 작성**
|
|
647
|
+
|
|
648
|
+
기존 시나리오(`tests-e2e/scenario-01-record-start-reconcile.sh`)의 골격(`mktemp -d` OKSTRA_HOME,
|
|
649
|
+
`trap` 정리)을 따른다. 검증 핵심:
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
#!/usr/bin/env bash
|
|
653
|
+
set -euo pipefail
|
|
654
|
+
OKSTRA_HOME="$(mktemp -d)"; export OKSTRA_HOME
|
|
655
|
+
trap 'rm -rf "$OKSTRA_HOME"' EXIT
|
|
656
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
657
|
+
|
|
658
|
+
# fan-out 산출물을 모사한 run 디렉토리를 만들고 validate_fanout 가 통과/거부하는지 확인.
|
|
659
|
+
WORK="$(mktemp -d)"; trap 'rm -rf "$OKSTRA_HOME" "$WORK"' EXIT
|
|
660
|
+
FO="$WORK/runs/requirements-discovery/fan-out"; mkdir -p "$FO"
|
|
661
|
+
cat > "$FO/unit-001.md" <<'EOF'
|
|
662
|
+
---
|
|
663
|
+
unit-id: unit-001
|
|
664
|
+
domain: bugfix
|
|
665
|
+
depends-on: []
|
|
666
|
+
recommended-next-phase: error-analysis
|
|
667
|
+
---
|
|
668
|
+
# unit-001
|
|
669
|
+
EOF
|
|
670
|
+
cat > "$FO/unit-002.md" <<'EOF'
|
|
671
|
+
---
|
|
672
|
+
unit-id: unit-002
|
|
673
|
+
domain: feature
|
|
674
|
+
depends-on: [unit-001]
|
|
675
|
+
recommended-next-phase: implementation-planning
|
|
676
|
+
---
|
|
677
|
+
# unit-002
|
|
678
|
+
EOF
|
|
679
|
+
printf '# Fan-out Index\n> generated\n1. unit-001\n2. unit-002\n' > "$FO/index.md"
|
|
680
|
+
|
|
681
|
+
python3 - "$WORK" <<'PY'
|
|
682
|
+
import sys
|
|
683
|
+
from pathlib import Path
|
|
684
|
+
sys.path.insert(0, str(Path("validators").resolve()))
|
|
685
|
+
from validate_fanout import validate_fanout
|
|
686
|
+
res = validate_fanout(Path(sys.argv[1]) / "runs" / "requirements-discovery")
|
|
687
|
+
assert res.ok, res.errors
|
|
688
|
+
print("fan-out validation OK")
|
|
689
|
+
PY
|
|
690
|
+
echo "PASS: scenario requirements-discovery-fanout"
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
- [ ] **Step 2: 실행 확인**
|
|
694
|
+
|
|
695
|
+
Run: `bash tests-e2e/scenario-<NN>-requirements-discovery-fanout.sh`
|
|
696
|
+
Expected: `PASS: scenario requirements-discovery-fanout`
|
|
697
|
+
|
|
698
|
+
- [ ] **Step 3: 커밋**
|
|
699
|
+
|
|
700
|
+
```bash
|
|
701
|
+
git add tests-e2e/scenario-<NN>-requirements-discovery-fanout.sh
|
|
702
|
+
git commit -m "test(e2e): requirements-discovery fan-out 검증 시나리오"
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Task 8: 전체 회귀 + 빌드
|
|
708
|
+
|
|
709
|
+
- [ ] **Step 1: 전체 유닛 스위트**
|
|
710
|
+
|
|
711
|
+
Run: `python3 -m pytest tests/ -q`
|
|
712
|
+
Expected: 기존 + 신규 모두 PASS (회귀 0)
|
|
713
|
+
|
|
714
|
+
- [ ] **Step 2: 빌드 + 워크플로 validator**
|
|
715
|
+
|
|
716
|
+
Run: `npm run build && bash validators/validate-workflow.sh`
|
|
717
|
+
Expected: build 성공, validator PASS
|
|
718
|
+
|
|
719
|
+
- [ ] **Step 3: CLI smoke**
|
|
720
|
+
|
|
721
|
+
Run: `node bin/okstra --version`
|
|
722
|
+
Expected: 버전 출력
|
|
723
|
+
|
|
724
|
+
- [ ] **Step 4: 최종 커밋 (남은 것 있으면)**
|
|
725
|
+
|
|
726
|
+
```bash
|
|
727
|
+
git status --short # 깨끗해야 함
|
|
728
|
+
```
|