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.
Files changed (161) hide show
  1. package/README.kr.md +5 -5
  2. package/README.md +5 -5
  3. package/docs/kr/architecture/storage-model.md +1 -1
  4. package/docs/kr/architecture.md +7 -11
  5. package/docs/kr/cli.md +4 -4
  6. package/docs/kr/container.md +1 -1
  7. package/docs/kr/performance-improvement-plan-v2.md +2 -2
  8. package/docs/project-structure-overview.md +2 -2
  9. package/package.json +7 -2
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/okstra-antigravity-exec.sh +1 -2
  12. package/runtime/bin/okstra-codex-exec.sh +1 -2
  13. package/runtime/bin/okstra-render-final-report.py +2 -9
  14. package/runtime/bin/okstra-spawn-followups.py +4 -6
  15. package/runtime/bin/okstra-trace-cleanup.sh +24 -4
  16. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  17. package/runtime/prompts/lead/report-writer.md +1 -1
  18. package/runtime/prompts/profiles/_common-contract.md +9 -11
  19. package/runtime/python/okstra_ctl/codex_dispatch.py +4 -12
  20. package/runtime/python/okstra_ctl/consumers.py +35 -0
  21. package/runtime/python/okstra_ctl/container.py +4 -6
  22. package/runtime/python/okstra_ctl/dispatch_core.py +4 -12
  23. package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
  24. package/runtime/python/okstra_ctl/handoff.py +2 -1
  25. package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
  26. package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
  27. package/runtime/python/okstra_ctl/report_views.py +7 -4
  28. package/runtime/python/okstra_ctl/run.py +3 -10
  29. package/runtime/python/okstra_ctl/stage_targets.py +18 -0
  30. package/runtime/python/okstra_ctl/wizard.py +24 -25
  31. package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
  32. package/runtime/python/okstra_token_usage/report.py +2 -5
  33. package/runtime/validators/validate-report-views.py +2 -1
  34. package/runtime/validators/validate-run.py +1 -11
  35. package/src/commands/lifecycle/install.mjs +5 -5
  36. package/src/commands/lifecycle/uninstall.mjs +4 -4
  37. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
  38. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
  39. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
  40. package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
  41. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
  42. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
  43. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
  44. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
  45. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
  46. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
  47. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
  48. package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
  49. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
  50. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
  51. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
  52. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
  53. package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
  54. package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
  55. package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
  56. package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
  57. package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
  58. package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
  59. package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
  60. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
  61. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
  62. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
  63. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
  64. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
  65. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
  66. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
  67. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
  68. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
  69. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
  70. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
  71. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
  72. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
  73. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
  74. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
  75. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
  76. package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
  77. package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
  78. package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
  79. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
  80. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
  81. package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
  82. package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
  83. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
  84. package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
  85. package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
  86. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
  87. package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
  88. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
  89. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
  90. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
  91. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
  92. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
  93. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
  94. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
  95. package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
  96. package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
  97. package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
  98. package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
  99. package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
  100. package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
  101. package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
  102. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
  103. package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
  104. package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
  105. package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
  106. package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
  107. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
  108. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
  109. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
  110. package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
  111. package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
  112. package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
  113. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
  114. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
  115. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
  116. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
  117. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
  118. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
  119. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
  120. package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
  121. package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
  122. package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
  123. package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
  124. package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
  125. package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
  126. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
  127. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
  128. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
  129. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
  130. package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
  131. package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
  132. package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
  133. package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
  134. package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
  135. package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
  136. package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
  137. package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
  138. package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
  139. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
  140. package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
  141. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
  142. package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
  143. package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
  144. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
  145. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
  146. package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
  147. package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
  148. package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
  149. package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
  150. package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
  151. package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
  152. package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
  153. package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
  154. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
  155. package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
  156. package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
  157. package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
  158. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
  159. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
  160. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
  161. 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 생성)을 작성한다.