okstra 0.102.2 → 0.102.3
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/kr/architecture/storage-model.md +1 -1
- package/docs/kr/architecture.md +7 -11
- package/docs/kr/cli.md +4 -4
- package/docs/kr/container.md +1 -1
- package/docs/kr/performance-improvement-plan-v2.md +2 -2
- package/docs/project-structure-overview.md +2 -2
- package/package.json +7 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-antigravity-exec.sh +1 -2
- package/runtime/bin/okstra-codex-exec.sh +1 -2
- package/runtime/bin/okstra-render-final-report.py +2 -9
- package/runtime/bin/okstra-spawn-followups.py +4 -6
- package/runtime/bin/okstra-trace-cleanup.sh +24 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +9 -11
- package/runtime/python/okstra_ctl/codex_dispatch.py +4 -12
- package/runtime/python/okstra_ctl/consumers.py +35 -0
- package/runtime/python/okstra_ctl/container.py +4 -6
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -12
- package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
- package/runtime/python/okstra_ctl/handoff.py +2 -1
- package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
- package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +7 -4
- package/runtime/python/okstra_ctl/run.py +3 -10
- package/runtime/python/okstra_ctl/stage_targets.py +18 -0
- package/runtime/python/okstra_ctl/wizard.py +24 -25
- package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
- package/runtime/python/okstra_token_usage/report.py +2 -5
- package/runtime/validators/validate-report-views.py +2 -1
- package/runtime/validators/validate-run.py +1 -11
- package/src/commands/lifecycle/install.mjs +5 -5
- package/src/commands/lifecycle/uninstall.mjs +4 -4
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
- package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
- package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
- package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
- package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
- package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
- package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
- package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
- package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
- package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
- package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
- package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
- package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
- package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
- package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
- package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
- package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
- package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
- package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
- package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
- package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
- package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
- package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
- package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
- package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
- package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
- package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
- package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
- package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
- package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
- package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
- package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
- package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
- package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
- package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
- package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
- package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
- package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
- package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
- package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
- package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
- package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
- package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
- package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
- package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
- package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
- package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
- package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
- package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
- package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
- package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
- package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
- package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
- package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
- package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
- package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
- package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
- package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
- package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
- package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
- package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
- package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
- package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
- package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
- package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
- package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
- package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
- package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
- package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
- package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
- package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
- package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
- package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
- package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
- package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
- package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
- package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
- package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
- package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
- package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
- package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
- package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
- package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
- package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
- package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
- package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
- package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
- package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
- package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
- package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
- package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
- package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
- package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
- package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
- package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
- package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +0 -110
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
# Stage Worktree 격리 P5 — 다중 의존 stage 자동 base 감지 구현 계획
|
|
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:** P3에서 `PrepareError`로 거부하던 다중 의존 stage(`depends-on X,Y…`)를 spec §9 옵션 A로 자동 처리한다 — task-key worktree HEAD를 candidate로 두고, 선행 stage들의 done commit이 모두 candidate의 ancestor면(=사용자가 머지함) candidate를 base로 발급, 아니면 머지 안내 `PrepareError`.
|
|
6
|
-
|
|
7
|
-
**Architecture:** 새 pure 헬퍼 `_commit_is_ancestor(project_root, ancestor, descendant)`가 `git merge-base --is-ancestor`를 감싼다. `_resolve_stage_base_commit`의 다중 의존 분기를 거부 → ancestor 검증으로 교체하고, 검증에 필요한 `candidate_base`/`project_root`를 새 인자로 받는다. `_reserve_implementation_stages`는 이미 계산한 task-key worktree HEAD(`head_sha`)를 candidate로, `inp.project_root`를 함께 넘긴다.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Python 3 (`scripts/okstra_ctl/run.py`), pytest (real `git init` repos).
|
|
10
|
-
|
|
11
|
-
설계 문서: [docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md](../specs/2026-06-06-stage-worktree-isolation-design.md) §2.2 / §9 #1
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## File Structure
|
|
16
|
-
|
|
17
|
-
- Modify: `scripts/okstra_ctl/run.py`
|
|
18
|
-
- 신규 `_commit_is_ancestor()` (모듈 helper, `_resolve_stage_base_commit` 직전)
|
|
19
|
-
- `_resolve_stage_base_commit()` — 다중 의존 분기 교체 + 인자 2개 추가
|
|
20
|
-
- `_reserve_implementation_stages()` — `_resolve_stage_base_commit` 호출에 candidate/project_root 전달
|
|
21
|
-
- Test:
|
|
22
|
-
- `tests/test_okstra_run_stage_base.py` (기존 — 다중 의존 케이스 교체 + ancestor 케이스 추가)
|
|
23
|
-
- `tests/test_okstra_run_reserve_implementation_stages.py` (기존 — 다중 의존 거부 테스트를 머지/미머지 두 케이스로)
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
### Task 1: `_commit_is_ancestor` 헬퍼 + `_resolve_stage_base_commit` 다중 의존 자동 감지
|
|
28
|
-
|
|
29
|
-
**Files:**
|
|
30
|
-
- Modify: `scripts/okstra_ctl/run.py`
|
|
31
|
-
- Test: `tests/test_okstra_run_stage_base.py`
|
|
32
|
-
|
|
33
|
-
- [ ] **Step 1: Replace the multi-dependency tests in `test_okstra_run_stage_base.py`**
|
|
34
|
-
|
|
35
|
-
기존 `test_multi_dependency_rejected_with_guidance` 함수를 삭제하고, 그 자리에 아래 4개를 넣는다. 파일 상단 import 에 `import subprocess` 와 `from pathlib import Path` 가 이미 있는지 확인하고 없으면 추가.
|
|
36
|
-
|
|
37
|
-
```python
|
|
38
|
-
def _init_repo_with_commits(repo: Path):
|
|
39
|
-
"""3개 커밋 c0→c1→c2 선형 히스토리를 만들고 (c0, c1, c2) sha 를 반환."""
|
|
40
|
-
subprocess.run(["git", "init", "-q", str(repo)], check=True)
|
|
41
|
-
subprocess.run(["git", "-C", str(repo), "config", "user.email", "t@e"], check=True)
|
|
42
|
-
subprocess.run(["git", "-C", str(repo), "config", "user.name", "t"], check=True)
|
|
43
|
-
shas = []
|
|
44
|
-
for i in range(3):
|
|
45
|
-
(repo / f"f{i}").write_text(f"{i}\n")
|
|
46
|
-
subprocess.run(["git", "-C", str(repo), "add", "."], check=True)
|
|
47
|
-
subprocess.run(["git", "-C", str(repo), "commit", "-q", "-m", f"c{i}"], check=True)
|
|
48
|
-
shas.append(subprocess.run(
|
|
49
|
-
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
|
50
|
-
capture_output=True, text=True, check=True).stdout.strip())
|
|
51
|
-
return shas # [c0, c1, c2]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_multi_dependency_returns_candidate_when_all_predecessors_ancestors(tmp_path):
|
|
55
|
-
repo = tmp_path / "repo"
|
|
56
|
-
c0, c1, c2 = _init_repo_with_commits(repo)
|
|
57
|
-
stage = _stage(4, [2, 3])
|
|
58
|
-
# 선행 2,3 의 done commit 이 c0,c1 — candidate c2 의 ancestor (선형 히스토리)
|
|
59
|
-
rows = [
|
|
60
|
-
{"stage": 2, "status": "done", "head_commit": c0},
|
|
61
|
-
{"stage": 3, "status": "done", "head_commit": c1},
|
|
62
|
-
]
|
|
63
|
-
got = _resolve_stage_base_commit(
|
|
64
|
-
stage, consumer_done_rows=rows, anchor_base_commit="unused",
|
|
65
|
-
candidate_base=c2, project_root=repo,
|
|
66
|
-
)
|
|
67
|
-
assert got == c2
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_multi_dependency_rejected_when_predecessor_not_merged(tmp_path):
|
|
71
|
-
repo = tmp_path / "repo"
|
|
72
|
-
c0, c1, c2 = _init_repo_with_commits(repo)
|
|
73
|
-
# 별도 분기 커밋 (candidate 의 ancestor 가 아님)
|
|
74
|
-
subprocess.run(["git", "-C", str(repo), "checkout", "-q", "-b", "side", c0], check=True)
|
|
75
|
-
(repo / "side").write_text("x\n")
|
|
76
|
-
subprocess.run(["git", "-C", str(repo), "add", "."], check=True)
|
|
77
|
-
subprocess.run(["git", "-C", str(repo), "commit", "-q", "-m", "side"], check=True)
|
|
78
|
-
side_sha = subprocess.run(
|
|
79
|
-
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
|
80
|
-
capture_output=True, text=True, check=True).stdout.strip()
|
|
81
|
-
stage = _stage(4, [2, 3])
|
|
82
|
-
rows = [
|
|
83
|
-
{"stage": 2, "status": "done", "head_commit": c1}, # ancestor of c2 ✓
|
|
84
|
-
{"stage": 3, "status": "done", "head_commit": side_sha}, # NOT ancestor of c2 ✗
|
|
85
|
-
]
|
|
86
|
-
with pytest.raises(PrepareError, match="not merged into the task worktree"):
|
|
87
|
-
_resolve_stage_base_commit(
|
|
88
|
-
stage, consumer_done_rows=rows, anchor_base_commit="unused",
|
|
89
|
-
candidate_base=c2, project_root=repo,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_multi_dependency_rejected_when_predecessor_not_done(tmp_path):
|
|
94
|
-
repo = tmp_path / "repo"
|
|
95
|
-
c0, c1, c2 = _init_repo_with_commits(repo)
|
|
96
|
-
stage = _stage(4, [2, 3])
|
|
97
|
-
rows = [{"stage": 2, "status": "done", "head_commit": c0}] # stage 3 done 없음
|
|
98
|
-
with pytest.raises(PrepareError, match="predecessor stage 3 has no done"):
|
|
99
|
-
_resolve_stage_base_commit(
|
|
100
|
-
stage, consumer_done_rows=rows, anchor_base_commit="unused",
|
|
101
|
-
candidate_base=c2, project_root=repo,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def test_multi_dependency_rejected_without_candidate(tmp_path):
|
|
106
|
-
repo = tmp_path / "repo"
|
|
107
|
-
c0, c1, c2 = _init_repo_with_commits(repo)
|
|
108
|
-
stage = _stage(4, [2, 3])
|
|
109
|
-
rows = [
|
|
110
|
-
{"stage": 2, "status": "done", "head_commit": c0},
|
|
111
|
-
{"stage": 3, "status": "done", "head_commit": c1},
|
|
112
|
-
]
|
|
113
|
-
with pytest.raises(PrepareError, match="candidate base missing"):
|
|
114
|
-
_resolve_stage_base_commit(
|
|
115
|
-
stage, consumer_done_rows=rows, anchor_base_commit="unused",
|
|
116
|
-
candidate_base="", project_root=repo,
|
|
117
|
-
)
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
121
|
-
|
|
122
|
-
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_stage_base.py -v`
|
|
123
|
-
Expected: 신규 4개 FAIL — `_resolve_stage_base_commit()` got unexpected keyword `candidate_base` (현재 시그니처에 없음). 기존 독립/단일 의존 테스트는 PASS 유지.
|
|
124
|
-
|
|
125
|
-
- [ ] **Step 3: Add `_commit_is_ancestor` helper + rewrite multi-dep branch**
|
|
126
|
-
|
|
127
|
-
`run.py`에서 `_resolve_stage_base_commit` 정의 **바로 위**에 헬퍼 추가:
|
|
128
|
-
|
|
129
|
-
```python
|
|
130
|
-
def _commit_is_ancestor(project_root, ancestor: str, descendant: str) -> bool:
|
|
131
|
-
"""True iff `ancestor` is an ancestor of `descendant` in project_root's git
|
|
132
|
-
history (`git merge-base --is-ancestor`, exit 0). Used to detect whether a
|
|
133
|
-
predecessor stage's done commit has been merged into the task worktree HEAD."""
|
|
134
|
-
r = _subprocess.run(
|
|
135
|
-
["git", "merge-base", "--is-ancestor", ancestor, descendant],
|
|
136
|
-
cwd=str(project_root), capture_output=True, text=True,
|
|
137
|
-
)
|
|
138
|
-
return r.returncode == 0
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
그리고 `_resolve_stage_base_commit`를 아래로 교체 (시그니처 + 다중 의존 분기):
|
|
142
|
-
|
|
143
|
-
```python
|
|
144
|
-
def _resolve_stage_base_commit(
|
|
145
|
-
stage: dict,
|
|
146
|
-
consumer_done_rows: list,
|
|
147
|
-
anchor_base_commit: str,
|
|
148
|
-
candidate_base: str = "",
|
|
149
|
-
project_root=None,
|
|
150
|
-
) -> str:
|
|
151
|
-
"""Pick the git base commit a stage's isolated worktree branches from.
|
|
152
|
-
|
|
153
|
-
- 독립 (`depends-on (none)`): anchor (= implementation_base_commit fixed
|
|
154
|
-
at first stage entry).
|
|
155
|
-
- 단일 의존 (`depends-on X`): predecessor X 의 `done.head_commit`.
|
|
156
|
-
- 다중 의존 (`depends-on X,Y…`): spec §9 옵션 A 자동 감지. candidate_base
|
|
157
|
-
(= task-key worktree HEAD) 에 모든 선행 done commit 이 ancestor 면
|
|
158
|
-
candidate 를 반환(사용자가 선행을 머지함). 아니면 PrepareError.
|
|
159
|
-
|
|
160
|
-
Raises PrepareError on missing predecessor / anchor / unmerged predecessor."""
|
|
161
|
-
deps = stage.get("depends_on") or []
|
|
162
|
-
if len(deps) >= 2:
|
|
163
|
-
n = stage["stage_number"]
|
|
164
|
-
# 1) 모든 선행의 done head_commit 수집
|
|
165
|
-
pred_commits = {}
|
|
166
|
-
for d in deps:
|
|
167
|
-
head = next(
|
|
168
|
-
(r.get("head_commit") for r in consumer_done_rows
|
|
169
|
-
if r.get("stage") == d and r.get("status") == "done"
|
|
170
|
-
and r.get("head_commit")),
|
|
171
|
-
None,
|
|
172
|
-
)
|
|
173
|
-
if not head:
|
|
174
|
-
raise PrepareError(
|
|
175
|
-
f"predecessor stage {d} has no done row with head_commit "
|
|
176
|
-
f"in consumers.jsonl; multi-dependency stage {n} cannot start"
|
|
177
|
-
)
|
|
178
|
-
pred_commits[d] = head
|
|
179
|
-
# 2) candidate (task-key worktree HEAD) 필요
|
|
180
|
-
if not candidate_base or project_root is None:
|
|
181
|
-
raise PrepareError(
|
|
182
|
-
f"candidate base missing for multi-dependency stage {n}; "
|
|
183
|
-
"task-key worktree HEAD could not be resolved"
|
|
184
|
-
)
|
|
185
|
-
# 3) 모든 선행 done 이 candidate 의 ancestor 인지 (=사용자가 머지함)
|
|
186
|
-
for d, head in pred_commits.items():
|
|
187
|
-
if not _commit_is_ancestor(project_root, head, candidate_base):
|
|
188
|
-
raise PrepareError(
|
|
189
|
-
f"multi-dependency stage {n}: predecessor stage {d} "
|
|
190
|
-
f"({head[:8]}) is not merged into the task worktree "
|
|
191
|
-
f"({candidate_base[:8]}). Merge stage branches "
|
|
192
|
-
f"(e.g. the `-s{d}` branches) into the task worktree "
|
|
193
|
-
"(or into main, then refresh the worktree) and retry."
|
|
194
|
-
)
|
|
195
|
-
return candidate_base
|
|
196
|
-
if not deps:
|
|
197
|
-
if not anchor_base_commit:
|
|
198
|
-
raise PrepareError(
|
|
199
|
-
f"anchor base commit missing for independent stage "
|
|
200
|
-
f"{stage['stage_number']}; first-stage prepare should have "
|
|
201
|
-
"fixed it via worktree_registry.set_implementation_base"
|
|
202
|
-
)
|
|
203
|
-
return anchor_base_commit
|
|
204
|
-
# 단일 의존
|
|
205
|
-
pred = deps[0]
|
|
206
|
-
for row in consumer_done_rows:
|
|
207
|
-
if row.get("stage") == pred and row.get("status") == "done":
|
|
208
|
-
head = row.get("head_commit") or ""
|
|
209
|
-
if head:
|
|
210
|
-
return head
|
|
211
|
-
raise PrepareError(
|
|
212
|
-
f"predecessor stage {pred} has no done row with head_commit in "
|
|
213
|
-
"consumers.jsonl; cannot derive base for stage "
|
|
214
|
-
f"{stage['stage_number']}"
|
|
215
|
-
)
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
219
|
-
|
|
220
|
-
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_stage_base.py -v`
|
|
221
|
-
Expected: PASS (독립/단일 기존 + 다중 의존 신규 4개).
|
|
222
|
-
|
|
223
|
-
- [ ] **Step 5: Commit**
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
cd /Volumes/Workspaces/workspace/projects/Okstra
|
|
227
|
-
git add scripts/okstra_ctl/run.py tests/test_okstra_run_stage_base.py
|
|
228
|
-
git commit -m "feat(run): auto-detect multi-dependency stage base via ancestor check"
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
### Task 2: `_reserve_implementation_stages` — candidate/project_root 전달 + e2e
|
|
234
|
-
|
|
235
|
-
**Files:**
|
|
236
|
-
- Modify: `scripts/okstra_ctl/run.py` (`_reserve_implementation_stages`의 `_resolve_stage_base_commit` 호출)
|
|
237
|
-
- Test: `tests/test_okstra_run_reserve_implementation_stages.py`
|
|
238
|
-
|
|
239
|
-
- [ ] **Step 1: Replace the multi-dep e2e test with merged/unmerged cases**
|
|
240
|
-
|
|
241
|
-
기존 `test_multi_dependency_stage_prepare_rejected` 함수를 삭제하고 아래 2개로 교체. (helper `_init_repo`, `_stage`, `_Inp`, `_consumers_dir` 는 그대로 사용.)
|
|
242
|
-
|
|
243
|
-
```python
|
|
244
|
-
def test_multi_dependency_starts_when_predecessors_merged(tmp_path):
|
|
245
|
-
"""선행 2,3 의 done commit 이 task worktree HEAD 의 ancestor 면 다중 의존
|
|
246
|
-
stage 4 가 그 HEAD 를 base 로 발급된다."""
|
|
247
|
-
repo = tmp_path / "repo"
|
|
248
|
-
base_sha = _init_repo(repo) # c0 (현재 HEAD)
|
|
249
|
-
plan_path = _consumers_dir(tmp_path)
|
|
250
|
-
|
|
251
|
-
from okstra_ctl.run import _reserve_implementation_stages
|
|
252
|
-
from okstra_ctl.worktree_registry import reserve as registry_reserve, lookup
|
|
253
|
-
from okstra_ctl.consumers import append_consumer
|
|
254
|
-
registry_reserve(
|
|
255
|
-
project_id="proj", task_group="grp", task_id="tid",
|
|
256
|
-
worktree_path=str(repo), branch="feat-tid", base_ref=base_sha,
|
|
257
|
-
)
|
|
258
|
-
# 선행 2,3 done — head_commit 을 base_sha(=현재 HEAD) 로 두면 HEAD 의 ancestor(자기 자신).
|
|
259
|
-
plan_run_root = Path(plan_path).resolve().parents[1]
|
|
260
|
-
append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
|
|
261
|
-
stage=2, status="done", head_commit=base_sha)
|
|
262
|
-
append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
|
|
263
|
-
stage=3, status="done", head_commit=base_sha)
|
|
264
|
-
|
|
265
|
-
ctx = {
|
|
266
|
-
"TASK_KEY": "proj/grp/tid",
|
|
267
|
-
"TASK_GROUP_SEGMENT": "grp", "TASK_ID_SEGMENT": "tid",
|
|
268
|
-
"EXECUTOR_WORKTREE_PATH": str(repo),
|
|
269
|
-
"EXECUTOR_WORKTREE_BRANCH": "feat-tid",
|
|
270
|
-
"EXECUTOR_WORKTREE_BASE_REF": base_sha,
|
|
271
|
-
"EXECUTOR_WORKTREE_STATUS": "created",
|
|
272
|
-
}
|
|
273
|
-
stages = [_stage(2, []), _stage(3, []), _stage(4, [2, 3])]
|
|
274
|
-
_reserve_implementation_stages(
|
|
275
|
-
_Inp(project_root=repo, approved_plan_path=str(plan_path), stage="4"),
|
|
276
|
-
ctx, stages,
|
|
277
|
-
)
|
|
278
|
-
assert ctx["EXECUTOR_WORKTREE_BRANCH"] == "feat-tid-s4"
|
|
279
|
-
assert lookup("proj", "grp", "tid", stage_number=4) is not None
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def test_multi_dependency_rejected_when_not_merged(tmp_path):
|
|
283
|
-
"""선행 done commit 이 task worktree HEAD 의 ancestor 가 아니면(미머지)
|
|
284
|
-
다중 의존 stage 가 PrepareError 로 거부된다."""
|
|
285
|
-
repo = tmp_path / "repo"
|
|
286
|
-
base_sha = _init_repo(repo)
|
|
287
|
-
plan_path = _consumers_dir(tmp_path)
|
|
288
|
-
|
|
289
|
-
from okstra_ctl.run import _reserve_implementation_stages, PrepareError
|
|
290
|
-
from okstra_ctl.worktree_registry import reserve as registry_reserve
|
|
291
|
-
from okstra_ctl.consumers import append_consumer
|
|
292
|
-
registry_reserve(
|
|
293
|
-
project_id="proj", task_group="grp", task_id="tid",
|
|
294
|
-
worktree_path=str(repo), branch="feat-tid", base_ref=base_sha,
|
|
295
|
-
)
|
|
296
|
-
plan_run_root = Path(plan_path).resolve().parents[1]
|
|
297
|
-
# 존재하지 않는/분기된 commit sha → HEAD 의 ancestor 아님
|
|
298
|
-
append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
|
|
299
|
-
stage=2, status="done", head_commit=base_sha)
|
|
300
|
-
append_consumer(plan_run_root, impl_task_key="proj/grp/tid",
|
|
301
|
-
stage=3, status="done",
|
|
302
|
-
head_commit="0000000000000000000000000000000000000000")
|
|
303
|
-
|
|
304
|
-
ctx = {
|
|
305
|
-
"TASK_KEY": "proj/grp/tid",
|
|
306
|
-
"TASK_GROUP_SEGMENT": "grp", "TASK_ID_SEGMENT": "tid",
|
|
307
|
-
"EXECUTOR_WORKTREE_PATH": str(repo),
|
|
308
|
-
"EXECUTOR_WORKTREE_BRANCH": "feat-tid",
|
|
309
|
-
"EXECUTOR_WORKTREE_BASE_REF": base_sha,
|
|
310
|
-
"EXECUTOR_WORKTREE_STATUS": "created",
|
|
311
|
-
}
|
|
312
|
-
stages = [_stage(2, []), _stage(3, []), _stage(4, [2, 3])]
|
|
313
|
-
with pytest.raises(PrepareError, match="not merged into the task worktree"):
|
|
314
|
-
_reserve_implementation_stages(
|
|
315
|
-
_Inp(project_root=repo, approved_plan_path=str(plan_path), stage="4"),
|
|
316
|
-
ctx, stages,
|
|
317
|
-
)
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
321
|
-
|
|
322
|
-
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_reserve_implementation_stages.py -v`
|
|
323
|
-
Expected: `test_multi_dependency_starts_when_predecessors_merged` FAILS (현재 `_reserve_implementation_stages`가 candidate/project_root 를 전달하지 않아 `_resolve_stage_base_commit`이 `candidate base missing` 으로 거부). `test_multi_dependency_rejected_when_not_merged` 는 이유는 다르지만(역시 candidate missing) 일단 raise 하므로 match 가 어긋나 FAIL 가능.
|
|
324
|
-
|
|
325
|
-
- [ ] **Step 3: Pass candidate_base + project_root in the call**
|
|
326
|
-
|
|
327
|
-
`_reserve_implementation_stages` 내부의 `_resolve_stage_base_commit(...)` 호출(현재):
|
|
328
|
-
|
|
329
|
-
```python
|
|
330
|
-
stage_base = _resolve_stage_base_commit(
|
|
331
|
-
selected_stage, consumer_done_rows, anchor_base_commit=anchor,
|
|
332
|
-
)
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
를 아래로 교체 (이미 함수 안에서 `head_sha`= task-key worktree HEAD 를 계산해 둠 — 그것을 candidate 로):
|
|
336
|
-
|
|
337
|
-
```python
|
|
338
|
-
stage_base = _resolve_stage_base_commit(
|
|
339
|
-
selected_stage, consumer_done_rows, anchor_base_commit=anchor,
|
|
340
|
-
candidate_base=head_sha, project_root=Path(inp.project_root),
|
|
341
|
-
)
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
> 참고: `head_sha`는 anchor 고정 블록에서 `git rev-parse HEAD`(in `inp.project_root`)로 이미 계산된 변수다. 다중 의존 stage 시점에는 사용자가 선행 stage 들을 worktree 에 머지했다면 이 HEAD 가 선행 done 들을 ancestor 로 가진다. 독립/단일 의존 경로는 candidate 를 쓰지 않으므로 무영향.
|
|
345
|
-
|
|
346
|
-
- [ ] **Step 4: Run target tests**
|
|
347
|
-
|
|
348
|
-
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/test_okstra_run_reserve_implementation_stages.py -v`
|
|
349
|
-
Expected: PASS (기존 + 신규 2개).
|
|
350
|
-
|
|
351
|
-
- [ ] **Step 5: Full regression**
|
|
352
|
-
|
|
353
|
-
Run: `cd /Volumes/Workspaces/workspace/projects/Okstra && python3 -m pytest tests/ -q`
|
|
354
|
-
Expected: PASS. 회귀 시 BLOCKED.
|
|
355
|
-
|
|
356
|
-
- [ ] **Step 6: Commit**
|
|
357
|
-
|
|
358
|
-
```bash
|
|
359
|
-
cd /Volumes/Workspaces/workspace/projects/Okstra
|
|
360
|
-
git add scripts/okstra_ctl/run.py tests/test_okstra_run_reserve_implementation_stages.py
|
|
361
|
-
git commit -m "feat(run): wire multi-dependency base auto-detection into stage reservation"
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
## Self-Review
|
|
367
|
-
|
|
368
|
-
**Spec coverage:**
|
|
369
|
-
- §9 #1 자동 감지: candidate=task-key HEAD, 선행 done 모두 ancestor → candidate, 아니면 PrepareError → Task 1 (헬퍼+분기) + Task 2 (wire-up) ✓
|
|
370
|
-
- §2.2 다중 의존 행 갱신 → 구현이 그 의미 ✓
|
|
371
|
-
- 옵션 B(octopus) → 비범위 유지 ✓
|
|
372
|
-
|
|
373
|
-
**Placeholder scan:** 없음 — 전 step 실제 코드.
|
|
374
|
-
|
|
375
|
-
**Type consistency:** `_commit_is_ancestor(project_root, ancestor, descendant) -> bool`; `_resolve_stage_base_commit(..., candidate_base="", project_root=None)`; 호출부가 `candidate_base=head_sha, project_root=Path(inp.project_root)`. P3에서 단일 의존/독립이 candidate 를 안 쓰므로 그 경로 무영향.
|
|
376
|
-
|
|
377
|
-
**위험 플래그(구현자 주의):**
|
|
378
|
-
- Task 2 e2e 는 실제 `git init` repo 의 HEAD(`base_sha`)를 선행 done commit 으로도 쓴다 — commit 은 자기 자신의 ancestor 이므로(`git merge-base --is-ancestor X X` exit 0) "머지됨" 케이스가 성립한다. 미머지 케이스는 all-zero sha 로 ancestor 검증을 실패시킨다(`merge-base --is-ancestor` 가 비존재 sha 에 비0 반환).
|
|
379
|
-
- `_resolve_stage_base_commit` 의 단일 의존/독립 분기는 **변경 금지** — 다중 의존 분기와 시그니처 인자 추가만.
|
|
380
|
-
|
|
381
|
-
## 검증 (P5 완료 기준)
|
|
382
|
-
|
|
383
|
-
```bash
|
|
384
|
-
cd /Volumes/Workspaces/workspace/projects/Okstra
|
|
385
|
-
python3 -m pytest tests/test_okstra_run_stage_base.py tests/test_okstra_run_reserve_implementation_stages.py -v
|
|
386
|
-
python3 -m pytest tests/ -q
|
|
387
|
-
```
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
# Stage Conformance QA — Phase 2 (게이트 판정 코어) Implementation Plan
|
|
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:** stage 별 conformance entry + 실행 결과를 받아 "PASS / WAIVED / EXEMPT / BLOCKING" 을 결정하는 **순수 판정 로직**을 `conformance.py` 에 추가한다(미실행·FAIL·MISSING = BLOCKING, 면제·waiver = 통과). 이게 DEV-9184 의 "silent mock-green" 을 막는 두뇌다.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Phase 1(매니페스트 검증 + `QA-RESULT` 파서)의 사이드카(부수효과 없는) 확장. wiring(verifier 가 결과 기록, validate-run 이 판정 적용, run.py 진입 검증, planning 이 스크립트 생성)은 **Phase 3** 로 분리한다. 본 Phase 는 외부 파일을 읽지 않는 순수 함수만 추가하므로 완전 TDD 가능하다. SSOT: [`docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md`](../specs/2026-06-07-stage-conformance-qa-design.md) §6(실행·판정), §7(우회).
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Python 3 (stdlib + pytest).
|
|
10
|
-
|
|
11
|
-
**전제:** Phase 1 완료 — `scripts/okstra_ctl/conformance.py` 에 `validate_conformance_manifest`, `parse_qa_result`, `QaResult`, `CAPABILITY_WHITELIST` 존재.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 판정 규칙 (spec §6, §7)
|
|
16
|
-
|
|
17
|
-
단일 stage entry + 그 stage 의 실행 결과(`QaResult | None`)로부터:
|
|
18
|
-
|
|
19
|
-
| 조건 | status | ok | conditional |
|
|
20
|
-
|---|---|---|---|
|
|
21
|
-
| `exemption` 있음 | EXEMPT | True | False |
|
|
22
|
-
| `waiver` 있음 | WAIVED | True | **True** (conformance 미검증 — 사용자 확인) |
|
|
23
|
-
| 결과 없음(`None`) | BLOCKING | False | False |
|
|
24
|
-
| `overall == "MISSING"` (마커 없음) | BLOCKING | False | False |
|
|
25
|
-
| `overall == "FAIL"` | BLOCKING | False | False |
|
|
26
|
-
| `overall == "PASS"` | PASS | True | False |
|
|
27
|
-
|
|
28
|
-
판정 우선순위: exemption → waiver → 결과 평가. (Phase 3 가 "면제 선언했지만 diff 가 실제 surface 를 건드림" 교차검증을 추가한다 — 본 Phase 는 선언을 신뢰한다.)
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Task 1: 게이트 판정 (`decide_conformance_gate`)
|
|
33
|
-
|
|
34
|
-
**Files:**
|
|
35
|
-
- Modify: `scripts/okstra_ctl/conformance.py`
|
|
36
|
-
- Test: `tests/test_okstra_ctl_conformance.py`
|
|
37
|
-
|
|
38
|
-
- [ ] **Step 1: 실패 테스트 추가**
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
# tests/test_okstra_ctl_conformance.py 의 import 에 추가
|
|
42
|
-
from okstra_ctl.conformance import ( # noqa: E402
|
|
43
|
-
ConformanceVerdict,
|
|
44
|
-
QaResult,
|
|
45
|
-
decide_conformance_gate,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _gate_entry(**over):
|
|
50
|
-
base = {"stageKey": "t-stage-1", "exemption": None, "waiver": None}
|
|
51
|
-
base.update(over)
|
|
52
|
-
return base
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_gate_pass_when_result_pass():
|
|
56
|
-
v = decide_conformance_gate(_gate_entry(), QaResult(overall="PASS", requirements={}))
|
|
57
|
-
assert isinstance(v, ConformanceVerdict)
|
|
58
|
-
assert v.status == "PASS" and v.ok is True and v.conditional is False
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_gate_blocking_when_no_result():
|
|
62
|
-
v = decide_conformance_gate(_gate_entry(), None)
|
|
63
|
-
assert v.status == "BLOCKING" and v.ok is False
|
|
64
|
-
assert "never ran" in v.message
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_gate_blocking_when_marker_missing():
|
|
68
|
-
v = decide_conformance_gate(_gate_entry(), QaResult(overall="MISSING", requirements={}))
|
|
69
|
-
assert v.status == "BLOCKING" and v.ok is False
|
|
70
|
-
assert "QA-RESULT" in v.message
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_gate_blocking_when_fail():
|
|
74
|
-
v = decide_conformance_gate(_gate_entry(), QaResult(overall="FAIL", requirements={}))
|
|
75
|
-
assert v.status == "BLOCKING" and v.ok is False
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_gate_exempt_passes_with_reason():
|
|
79
|
-
entry = _gate_entry(exemption={"reason": "doc-only", "declaredAt": "2026-06-07"})
|
|
80
|
-
v = decide_conformance_gate(entry, None)
|
|
81
|
-
assert v.status == "EXEMPT" and v.ok is True and v.conditional is False
|
|
82
|
-
assert "doc-only" in v.message
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_gate_waiver_is_conditional():
|
|
86
|
-
entry = _gate_entry(waiver={"acknowledgedBy": "user", "reason": "replica down",
|
|
87
|
-
"at": "2026-06-07", "scope": ["db"]})
|
|
88
|
-
v = decide_conformance_gate(entry, None)
|
|
89
|
-
assert v.status == "WAIVED" and v.ok is True and v.conditional is True
|
|
90
|
-
assert "user" in v.message and "replica down" in v.message
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_gate_exemption_takes_priority_over_failing_result():
|
|
94
|
-
entry = _gate_entry(exemption={"reason": "n/a", "declaredAt": "2026-06-07"})
|
|
95
|
-
v = decide_conformance_gate(entry, QaResult(overall="FAIL", requirements={}))
|
|
96
|
-
assert v.status == "EXEMPT" and v.ok is True
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
- [ ] **Step 2: 실패 확인**
|
|
100
|
-
|
|
101
|
-
Run: `python3 -m pytest tests/test_okstra_ctl_conformance.py -q`
|
|
102
|
-
Expected: FAIL — `cannot import name 'ConformanceVerdict'` / `decide_conformance_gate`
|
|
103
|
-
|
|
104
|
-
- [ ] **Step 3: 구현 추가**
|
|
105
|
-
|
|
106
|
-
`conformance.py` 의 `parse_qa_result` 정의 다음에 추가:
|
|
107
|
-
|
|
108
|
-
```python
|
|
109
|
-
@dataclass
|
|
110
|
-
class ConformanceVerdict:
|
|
111
|
-
stage_key: str
|
|
112
|
-
status: str # "PASS" | "BLOCKING" | "WAIVED" | "EXEMPT"
|
|
113
|
-
ok: bool # 진행 허용 여부 (PASS/WAIVED/EXEMPT 면 True)
|
|
114
|
-
conditional: bool # WAIVED 일 때만 True — conformance 미검증(사용자 확인)
|
|
115
|
-
message: str
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def decide_conformance_gate(entry: dict, result: object) -> ConformanceVerdict:
|
|
119
|
-
"""단일 stage entry + 실행 결과(`QaResult | None`)로 게이트 판정.
|
|
120
|
-
|
|
121
|
-
우선순위: exemption → waiver → 결과 평가. 미실행/MISSING/FAIL 은 BLOCKING.
|
|
122
|
-
면제·waiver 의 형태 검증은 `validate_conformance_manifest` 가 이미 보장한다.
|
|
123
|
-
"""
|
|
124
|
-
key = entry.get("stageKey", "<unknown>")
|
|
125
|
-
exemption = entry.get("exemption")
|
|
126
|
-
if exemption:
|
|
127
|
-
return ConformanceVerdict(
|
|
128
|
-
key, "EXEMPT", True, False,
|
|
129
|
-
f"conformance exempted: {exemption.get('reason', '')}",
|
|
130
|
-
)
|
|
131
|
-
waiver = entry.get("waiver")
|
|
132
|
-
if waiver:
|
|
133
|
-
return ConformanceVerdict(
|
|
134
|
-
key, "WAIVED", True, True,
|
|
135
|
-
f"conformance waived by {waiver.get('acknowledgedBy', '?')}: "
|
|
136
|
-
f"{waiver.get('reason', '')}",
|
|
137
|
-
)
|
|
138
|
-
overall = getattr(result, "overall", None) if result is not None else None
|
|
139
|
-
if overall == "PASS":
|
|
140
|
-
return ConformanceVerdict(key, "PASS", True, False, "conformance PASS")
|
|
141
|
-
if overall is None:
|
|
142
|
-
return ConformanceVerdict(
|
|
143
|
-
key, "BLOCKING", False, False,
|
|
144
|
-
"conformance script never ran (no result recorded)",
|
|
145
|
-
)
|
|
146
|
-
if overall == "MISSING":
|
|
147
|
-
return ConformanceVerdict(
|
|
148
|
-
key, "BLOCKING", False, False,
|
|
149
|
-
"conformance script ran but emitted no QA-RESULT marker",
|
|
150
|
-
)
|
|
151
|
-
return ConformanceVerdict(key, "BLOCKING", False, False, f"conformance {overall}")
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
- [ ] **Step 4: 통과 확인**
|
|
155
|
-
|
|
156
|
-
Run: `python3 -m pytest tests/test_okstra_ctl_conformance.py -q`
|
|
157
|
-
Expected: PASS (24 passed — 기존 17 + 신규 7)
|
|
158
|
-
|
|
159
|
-
- [ ] **Step 5: 커밋**
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
git add scripts/okstra_ctl/conformance.py tests/test_okstra_ctl_conformance.py
|
|
163
|
-
git commit -m "feat(okstra_ctl/conformance): stage 게이트 판정(decide_conformance_gate)"
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Task 2: 사이드카 변환 + 매니페스트 일괄 평가
|
|
167
|
-
|
|
168
|
-
**Files:**
|
|
169
|
-
- Modify: `scripts/okstra_ctl/conformance.py`
|
|
170
|
-
- Test: `tests/test_okstra_ctl_conformance.py`
|
|
171
|
-
|
|
172
|
-
- [ ] **Step 1: 실패 테스트 추가**
|
|
173
|
-
|
|
174
|
-
```python
|
|
175
|
-
from okstra_ctl.conformance import ( # noqa: E402 (위 import 에 합쳐도 됨)
|
|
176
|
-
evaluate_conformance,
|
|
177
|
-
qa_result_from_dict,
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def test_qa_result_from_dict_roundtrip():
|
|
182
|
-
r = qa_result_from_dict({"overall": "PASS", "requirements": {"R-001": {"status": "PASS"}}})
|
|
183
|
-
assert r.overall == "PASS" and r.requirements["R-001"]["status"] == "PASS"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def test_qa_result_from_dict_defaults_to_missing():
|
|
187
|
-
assert qa_result_from_dict(None).overall == "MISSING"
|
|
188
|
-
assert qa_result_from_dict({"overall": "bogus"}).overall == "MISSING"
|
|
189
|
-
assert qa_result_from_dict({}).requirements == {}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def test_evaluate_conformance_mixes_verdicts():
|
|
193
|
-
manifest = {"entries": [
|
|
194
|
-
{"stageKey": "t-stage-1", "exemption": None, "waiver": None}, # PASS
|
|
195
|
-
{"stageKey": "t-stage-2", "exemption": None, "waiver": None}, # BLOCKING (no result)
|
|
196
|
-
{"stageKey": "t-stage-3",
|
|
197
|
-
"exemption": {"reason": "doc", "declaredAt": "x"}, "waiver": None}, # EXEMPT
|
|
198
|
-
]}
|
|
199
|
-
results = {"t-stage-1": QaResult(overall="PASS", requirements={})}
|
|
200
|
-
verdicts = evaluate_conformance(manifest, results)
|
|
201
|
-
by_key = {v.stage_key: v for v in verdicts}
|
|
202
|
-
assert by_key["t-stage-1"].status == "PASS"
|
|
203
|
-
assert by_key["t-stage-2"].status == "BLOCKING"
|
|
204
|
-
assert by_key["t-stage-3"].status == "EXEMPT"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def test_evaluate_conformance_empty_manifest():
|
|
208
|
-
assert evaluate_conformance(None, {}) == []
|
|
209
|
-
assert evaluate_conformance({"entries": []}, {}) == []
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
- [ ] **Step 2: 실패 확인**
|
|
213
|
-
|
|
214
|
-
Run: `python3 -m pytest tests/test_okstra_ctl_conformance.py -q`
|
|
215
|
-
Expected: FAIL — `cannot import name 'evaluate_conformance'` / `qa_result_from_dict`
|
|
216
|
-
|
|
217
|
-
- [ ] **Step 3: 구현 추가**
|
|
218
|
-
|
|
219
|
-
`conformance.py` 의 `decide_conformance_gate` 다음에 추가:
|
|
220
|
-
|
|
221
|
-
```python
|
|
222
|
-
def qa_result_from_dict(data: object) -> QaResult:
|
|
223
|
-
"""결과 사이드카(JSON dict)를 `QaResult` 로 복원. Phase 3 의 verifier 가 쓴
|
|
224
|
-
`result-stage-<N>.json` 을 validate-run 이 로드할 때 쓴다. 형태가 깨졌으면
|
|
225
|
-
overall='MISSING'(=BLOCKING 취급)으로 안전하게 강등한다."""
|
|
226
|
-
if not isinstance(data, dict):
|
|
227
|
-
return QaResult(overall="MISSING", requirements={})
|
|
228
|
-
overall = data.get("overall")
|
|
229
|
-
if overall not in ("PASS", "FAIL", "MISSING"):
|
|
230
|
-
overall = "MISSING"
|
|
231
|
-
reqs = data.get("requirements")
|
|
232
|
-
return QaResult(overall=overall, requirements=reqs if isinstance(reqs, dict) else {})
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def evaluate_conformance(manifest: object, results_by_stage: object) -> list[ConformanceVerdict]:
|
|
236
|
-
"""매니페스트 전 entry 에 대해 게이트 판정 목록을 반환.
|
|
237
|
-
|
|
238
|
-
`results_by_stage`: stageKey -> `QaResult`. 키가 없으면 미실행(None)으로 본다.
|
|
239
|
-
매니페스트 구조 검증은 호출 전에 `validate_conformance_manifest` 로 끝낸다는 전제.
|
|
240
|
-
"""
|
|
241
|
-
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
242
|
-
if not isinstance(entries, list):
|
|
243
|
-
return []
|
|
244
|
-
results = results_by_stage if isinstance(results_by_stage, dict) else {}
|
|
245
|
-
verdicts: list[ConformanceVerdict] = []
|
|
246
|
-
for entry in entries:
|
|
247
|
-
if not isinstance(entry, dict):
|
|
248
|
-
continue
|
|
249
|
-
result = results.get(entry.get("stageKey"))
|
|
250
|
-
verdicts.append(decide_conformance_gate(entry, result))
|
|
251
|
-
return verdicts
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
- [ ] **Step 4: 통과 확인 + 전체 회귀**
|
|
255
|
-
|
|
256
|
-
Run: `python3 -m pytest tests/test_okstra_ctl_conformance.py -q` → Expected: PASS (28 passed)
|
|
257
|
-
Run: `python3 -m pytest tests/ -q` → Expected: 전부 통과(회귀 없음)
|
|
258
|
-
|
|
259
|
-
- [ ] **Step 5: 커밋**
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
git add scripts/okstra_ctl/conformance.py tests/test_okstra_ctl_conformance.py
|
|
263
|
-
git commit -m "feat(okstra_ctl/conformance): 사이드카 변환 + evaluate_conformance 일괄 평가"
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## Phase 2 Self-Review 체크
|
|
269
|
-
|
|
270
|
-
- [ ] spec §6 의 미실행/MISSING/FAIL = BLOCKING, PASS = 통과가 모두 커버되는가?
|
|
271
|
-
- [ ] spec §7 의 면제(EXEMPT) / waiver(WAIVED, conditional) 가 커버되는가?
|
|
272
|
-
- [ ] Phase 3 가 의존할 공개 API(`decide_conformance_gate`, `evaluate_conformance`, `qa_result_from_dict`, `ConformanceVerdict`)가 노출되는가?
|
|
273
|
-
- [ ] 순수 함수만 추가했는가(파일 I/O·외부 의존 없음)? — wiring 은 Phase 3.
|
|
274
|
-
|
|
275
|
-
Phase 2 완료 후 Phase 3 계획(verifier 결과 기록 + validate-run 게이트 적용 + run.py 진입 검증 + planning 생성)을 작성한다.
|