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