okstra 0.101.0 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/docs/kr/architecture.md +1 -1
  2. package/docs/project-structure-overview.md +1 -1
  3. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +542 -0
  4. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +568 -0
  5. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +203 -0
  6. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +129 -0
  7. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +185 -0
  8. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +121 -0
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/report-writer-worker.md +1 -0
  12. package/runtime/prompts/lead/report-writer.md +1 -1
  13. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
  14. package/runtime/prompts/profiles/final-verification.md +4 -3
  15. package/runtime/prompts/profiles/implementation-planning.md +12 -9
  16. package/runtime/python/okstra_ctl/report_views.py +39 -3
  17. package/runtime/python/okstra_ctl/run.py +0 -1
  18. package/runtime/schemas/final-report-v1.0.schema.json +106 -3
  19. package/runtime/skills/okstra-container/SKILL.md +1 -1
  20. package/runtime/templates/reports/final-report.template.md +63 -2
  21. package/runtime/templates/reports/final-verification-input.template.md +3 -3
  22. package/runtime/templates/reports/i18n/en.json +28 -1
  23. package/runtime/templates/reports/i18n/ko.json +28 -1
  24. package/runtime/templates/reports/report.css +10 -0
  25. package/runtime/templates/reports/report.js +16 -0
  26. package/runtime/templates/worker-prompt-preamble.md +1 -1
  27. package/runtime/validators/validate-run.py +70 -1
@@ -217,6 +217,22 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
217
217
  {% endfor %}
218
218
  {%- endif %}
219
219
 
220
+ ### Cross-Project Dependencies{% if t("sectionAside.crossProjectDependencies") != "Cross-Project Dependencies" %} ({{ t("sectionAside.crossProjectDependencies") }}){% endif %}
221
+
222
+ {% if implementationPlanning.crossProjectDependencies | length == 0 -%}
223
+ {{ t("emptyState.crossProjectDependencies") }}
224
+ {%- else %}
225
+ {% for xp in implementationPlanning.crossProjectDependencies %}
226
+ **{{ xp.id }} — [{{ xp.direction }}] {{ xp.project }}**
227
+
228
+ - {{ t("implementationPlanning.crossProjectFields.requiredWork") }}: {{ xp.requiredWork }}
229
+ - {{ t("implementationPlanning.crossProjectFields.verificationSignal") }}: {{ xp.verificationSignal }}
230
+ - {{ t("implementationPlanning.crossProjectFields.linkedWork") }}: {{ xp.linkedWork }}
231
+ - {{ t("implementationPlanning.crossProjectFields.howToStart") }}: {{ xp.howToStart }}
232
+ {% endfor %}
233
+ {{ t("implementationPlanning.crossProjectRecognitionNote") }}
234
+ {%- endif %}
235
+
220
236
  ### Validation Checklist{% if t("sectionAside.validationChecklist") != "Validation Checklist" %} ({{ t("sectionAside.validationChecklist") }}){% endif %}
221
237
 
222
238
  | ID | Phase | Ticket ID | Check | Command / Observation | Expected outcome |
@@ -241,6 +257,25 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
241
257
  | {{ row.id | mdcell }} | `{{ row.source | mdcell }}` | {{ row.requirement | mdcell }} | {{ row.coveredBy | mdcell }} | `{{ row.status | mdcell }}` |
242
258
  {% endfor %}
243
259
 
260
+ ### Decision Drafts{% if t("sectionAside.decisionDrafts") != "Decision Drafts" %} ({{ t("sectionAside.decisionDrafts") }}){% endif %}
261
+
262
+ {% if (implementationPlanning.decisionDrafts | length == 0) and (implementationPlanning.skippedAdrCandidates | length == 0) -%}
263
+ {{ t("emptyState.decisionDrafts") }}
264
+ {%- else %}
265
+ {% for d in implementationPlanning.decisionDrafts %}
266
+ #### {{ d.number }}-{{ d.slug }}
267
+
268
+ - **{{ t("implementationPlanning.decisionDraftFields.status") }}:** {{ d.status }}
269
+ - **{{ t("implementationPlanning.decisionDraftFields.context") }}:** {{ d.context }}
270
+ - **{{ t("implementationPlanning.decisionDraftFields.decision") }}:** {{ d.decision }}
271
+ - **{{ t("implementationPlanning.decisionDraftFields.consequences") }}:** {{ d.consequences }}
272
+ - **{{ t("implementationPlanning.decisionDraftFields.alternativesConsidered") }}:** {{ d.alternativesConsidered }}
273
+ {% endfor %}
274
+ {% for s in implementationPlanning.skippedAdrCandidates %}
275
+ - skipped adr-candidate: {{ s.topic }} — reason: {{ s.reason }}
276
+ {% endfor %}
277
+ {%- endif %}
278
+
244
279
  ## 5.5 Stage Map
245
280
 
246
281
  | stage | title | depends-on | step-count | exit-contract-summary |
@@ -482,7 +517,21 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
482
517
  | {{ row.category | mdcell }} | `{{ row.rollbackCommand | mdcell }}` | {{ row.verification | mdcell }} | `{{ row.result | mdcell }}` |
483
518
  {% endfor %}
484
519
 
485
- ### 5.7.9 Routing Recommendation
520
+ ### 5.7.9 Manual User Test (Draft)
521
+
522
+ {% if implementation.manualUserTest.applicable -%}
523
+ {% for item in implementation.manualUserTest.items %}
524
+ - **{{ item.target }}**
525
+ - {{ t("manualUserTest.environmentSetup") }}: {{ item.environmentSetup }}
526
+ - {{ t("manualUserTest.steps") }}:
527
+ {% for step in item.steps %} {{ loop.index }}. {{ step }}
528
+ {% endfor %} - {{ t("manualUserTest.expectedResult") }}: {{ item.expectedResult }}
529
+ {% endfor %}
530
+ {%- else -%}
531
+ - {{ t("manualUserTest.notApplicable") }}: {{ implementation.manualUserTest.exemptionReason }}
532
+ {%- endif %}
533
+
534
+ ### 5.7.10 Routing Recommendation
486
535
 
487
536
  {{ implementation.routingRecommendation }}
488
537
 
@@ -565,7 +614,19 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
565
614
  {% endfor %}
566
615
  {%- endif %}
567
616
 
568
- ### 5.8.7 Routing Recommendation
617
+ ### 5.8.7 Manual User Test Results
618
+
619
+ {% if finalVerification.manualUserTest.applicable -%}
620
+ | {{ t("manualUserTest.performed") }} | {{ t("manualUserTest.result") }} | {{ t("manualUserTest.observed") }} |
621
+ |---|---|---|
622
+ {% for row in finalVerification.manualUserTest.results -%}
623
+ | {{ row.target | mdcell }} — {{ row.performed | mdcell }} | `{{ row.result | mdcell }}` | {{ row.observed | mdcell }} |
624
+ {% endfor %}
625
+ {%- else -%}
626
+ - {{ t("manualUserTest.notApplicable") }}: {{ finalVerification.manualUserTest.exemptionReaffirm }}
627
+ {%- endif %}
628
+
629
+ ### 5.8.8 Routing Recommendation
569
630
 
570
631
  {{ finalVerification.routingRecommendation }}
571
632
 
@@ -40,7 +40,7 @@ taskType: "{{FM_TASK_TYPE}}"
40
40
  - Path (project-relative) to the originating `implementation` final-report:
41
41
  - Quoted `Commit list` / `Diff summary` excerpt from the implementation report:
42
42
 
43
- > 보고서 경로가 비거나 누락된 보고서를 가리키면 final-verification 은 status `blocked` 으로 끝내고 `implementation` 또는 `implementation-planning` 으로 라우팅합니다. 검증 대상(worktree/base/head)은 okstra 가 자동 해소하므로 수동 기입이 어긋나 막히는 일은 없습니다.
43
+ > 보고서 경로가 비거나 누락된 보고서를 가리키면 final-verification 은 status `blocked` 으로 끝내고 `implementation` 또는 `implementation-planning` 으로 라우팅합니다. 검증 대상(worktree/base)은 okstra 가 자동 해소하므로 수동 기입이 어긋나 막히는 일은 없습니다.
44
44
 
45
45
  ## Requirement Coverage Source
46
46
 
@@ -55,7 +55,7 @@ taskType: "{{FM_TASK_TYPE}}"
55
55
 
56
56
  - PR or change summary:
57
57
  - Test results:
58
- - Manual verification notes:
58
+ - Manual verification notes (execute the source implementation report's §5.7.9 Manual User Test Draft and record results here):
59
59
  - Related code paths:
60
60
  - Release or rollout notes:
61
61
  - Previous reports in the same task history:
@@ -90,7 +90,7 @@ taskType: "{{FM_TASK_TYPE}}"
90
90
 
91
91
  ## Questions for Analysers
92
92
 
93
- 1. Did your analysis run against the injected `VERIFICATION_TARGET` (base / head SHA / worktree), and does the diff at that target fully cover the stage(s) under verification? (A head you cannot confirm against the injected target is a `tool-failure`, not a silent proceed.)
93
+ 1. Did your analysis run against the injected `VERIFICATION_TARGET` (base ref / worktree), and does the worktree's delivered diff fully cover the stage(s) under verification? (A worktree you cannot confirm against the injected target is a `tool-failure`, not a silent proceed.)
94
94
  2. For each requirement / acceptance criterion, what exact artifact (commit SHA, test output, log line, config value) proves coverage?
95
95
  3. Are there any acceptance blockers?
96
96
  4. What residual risks remain?
@@ -9,6 +9,8 @@
9
9
  "primaryEvidence": "- No primary evidence.",
10
10
  "secondaryEvidence": "- No secondary evidence or alternate interpretations.",
11
11
  "risks": "- No missing information or risks.",
12
+ "crossProjectDependencies": "- No upstream/downstream dependency on another project.",
13
+ "decisionDrafts": "- No decision drafts or skipped ADR candidates.",
12
14
  "dependencyRisk": "- No dependency / migration risks.",
13
15
  "dissent": "- No dissenting opinions.",
14
16
  "outOfPlanEdits": "- No out-of-plan edits.",
@@ -29,6 +31,8 @@
29
31
  "checkMethod": "How to check"
30
32
  },
31
33
  "sectionAside": {
34
+ "crossProjectDependencies": "Cross-Project Dependencies",
35
+ "decisionDrafts": "Decision Drafts",
32
36
  "dependencyRisk": "Dependency / Migration Risk",
33
37
  "validationChecklist": "Validation Checklist",
34
38
  "rollbackStrategy": "Rollback Strategy",
@@ -95,7 +99,21 @@
95
99
  "rationaleLabel": "Rationale (trade-off row / principle)",
96
100
  "rejectedSummaryLabel": "Summary of rejected options",
97
101
  "columnImpact": "Impact",
98
- "columnMitigation": "Mitigation / prerequisite"
102
+ "columnMitigation": "Mitigation / prerequisite",
103
+ "crossProjectFields": {
104
+ "requiredWork": "Required work",
105
+ "verificationSignal": "Verification signal",
106
+ "linkedWork": "Linked work",
107
+ "howToStart": "How to start"
108
+ },
109
+ "crossProjectRecognitionNote": "Note: the other project's run is structurally independent — its done-set, stage numbers, and depends-on all resolve against its own repo, and this run's completed stages are NOT auto-recognized as done there. This section is narrative input that seeds the other run's planning only.",
110
+ "decisionDraftFields": {
111
+ "status": "Status",
112
+ "context": "Context",
113
+ "decision": "Decision",
114
+ "consequences": "Consequences",
115
+ "alternativesConsidered": "Alternatives Considered"
116
+ }
99
117
  },
100
118
  "releaseHandoff": {
101
119
  "auditNote": "Audit trail for the phase that ran git/gh mutating commands.",
@@ -145,6 +163,15 @@
145
163
  "verificationScope": "Verification scope",
146
164
  "stageReportsLabel": "Source implementation reports (per stage)"
147
165
  },
166
+ "manualUserTest": {
167
+ "environmentSetup": "Environment",
168
+ "steps": "Steps",
169
+ "expectedResult": "Expected result",
170
+ "notApplicable": "Manual user test not required",
171
+ "performed": "Performed",
172
+ "result": "Result",
173
+ "observed": "Observed"
174
+ },
148
175
  "index": {
149
176
  "heading": "Index",
150
177
  "sectionsLabel": "Sections",
@@ -9,6 +9,8 @@
9
9
  "primaryEvidence": "- 주 증거 없음.",
10
10
  "secondaryEvidence": "- 보조 증거 또는 대안 해석 없음.",
11
11
  "risks": "- 누락된 정보·위험 없음.",
12
+ "crossProjectDependencies": "- 다른 프로젝트에 대한 선행/후속 의존 없음.",
13
+ "decisionDrafts": "- 결정 기록 초안이나 스킵된 ADR 후보 없음.",
12
14
  "dependencyRisk": "- 의존성·마이그레이션 위험 없음.",
13
15
  "dissent": "- 반대 의견 없음.",
14
16
  "outOfPlanEdits": "- 계획 외 편집 없음.",
@@ -29,6 +31,8 @@
29
31
  "checkMethod": "확인 방법"
30
32
  },
31
33
  "sectionAside": {
34
+ "crossProjectDependencies": "프로젝트 간 의존성",
35
+ "decisionDrafts": "결정 기록 초안",
32
36
  "dependencyRisk": "의존성·마이그레이션 위험",
33
37
  "validationChecklist": "검증 체크리스트",
34
38
  "rollbackStrategy": "롤백 전략",
@@ -95,7 +99,21 @@
95
99
  "rationaleLabel": "근거 (Trade-off 행 / 원칙)",
96
100
  "rejectedSummaryLabel": "채택되지 않은 옵션 요약",
97
101
  "columnImpact": "영향",
98
- "columnMitigation": "완화 / 선행 작업"
102
+ "columnMitigation": "완화 / 선행 작업",
103
+ "crossProjectFields": {
104
+ "requiredWork": "필요 작업",
105
+ "verificationSignal": "검증 신호",
106
+ "linkedWork": "연결된 작업",
107
+ "howToStart": "시작 방법"
108
+ },
109
+ "crossProjectRecognitionNote": "참고: 상대 프로젝트의 run 은 구조적으로 독립이다 — done-set·stage 번호·depends-on 이 모두 자기 repo 기준이며, 이 run 의 완료 stage 가 자동으로 done 으로 인식되지 않는다. 이 섹션은 상대 run 의 planning 을 seed 하는 narrative 입력일 뿐이다.",
110
+ "decisionDraftFields": {
111
+ "status": "상태",
112
+ "context": "맥락",
113
+ "decision": "결정",
114
+ "consequences": "결과",
115
+ "alternativesConsidered": "검토한 대안"
116
+ }
99
117
  },
100
118
  "releaseHandoff": {
101
119
  "auditNote": "git/gh mutating 명령이 실행된 phase 의 감사 기록.",
@@ -145,6 +163,15 @@
145
163
  "verificationScope": "검증 범위",
146
164
  "stageReportsLabel": "stage 별 구현 리포트"
147
165
  },
166
+ "manualUserTest": {
167
+ "environmentSetup": "환경 기동",
168
+ "steps": "확인 스텝",
169
+ "expectedResult": "기대 결과",
170
+ "notApplicable": "사용자 테스트 불필요",
171
+ "performed": "수행",
172
+ "result": "결과",
173
+ "observed": "관찰값"
174
+ },
148
175
  "index": {
149
176
  "heading": "목차",
150
177
  "sectionsLabel": "섹션",
@@ -197,6 +197,16 @@ button[data-action]:hover { background: color-mix(in srgb, Highlight 20%, Button
197
197
  }
198
198
  #user-response-output:empty { display: none; }
199
199
 
200
+ .user-response-dismiss {
201
+ position: absolute;
202
+ top: 0.35em;
203
+ right: 0.5em;
204
+ padding: 0 0.45em;
205
+ line-height: 1.5;
206
+ font-size: 1.05rem;
207
+ font-weight: 600;
208
+ }
209
+
200
210
  #plan-approval {
201
211
  max-width: 120ch;
202
212
  margin: 1.5rem auto;
@@ -187,6 +187,7 @@
187
187
  var md = buildUserResponseMarkdown(runMeta, entries, isoNowUtc(), approval);
188
188
  var out = document.getElementById("user-response-output");
189
189
  if (out) out.textContent = md;
190
+ showDismiss(true);
190
191
  // Nothing answered yet — show the empty serialisation as feedback but
191
192
  // don't download a useless file.
192
193
  if (entries.length > 0 || approval) downloadUserResponse(md, runMeta);
@@ -196,6 +197,7 @@
196
197
  function copyUserResponse() {
197
198
  var out = document.getElementById("user-response-output");
198
199
  if (!out || !out.textContent) return;
200
+ showDismiss(true);
199
201
  var text = out.textContent;
200
202
  if (navigator.clipboard && navigator.clipboard.writeText) {
201
203
  navigator.clipboard.writeText(text).catch(function () {
@@ -217,6 +219,17 @@
217
219
  document.body.removeChild(ta);
218
220
  }
219
221
 
222
+ function showDismiss(show) {
223
+ var btn = document.querySelector('button[data-action="dismiss-user-response"]');
224
+ if (btn) btn.hidden = !show;
225
+ }
226
+
227
+ function dismissUserResponse() {
228
+ var out = document.getElementById("user-response-output");
229
+ if (out) out.textContent = "";
230
+ showDismiss(false);
231
+ }
232
+
220
233
  function bind() {
221
234
  var clickables = document.querySelectorAll("button[data-action]");
222
235
  for (var i = 0; i < clickables.length; i++) {
@@ -226,7 +239,10 @@
226
239
  btn.addEventListener("click", exportUserResponse);
227
240
  } else if (action === "copy-user-response") {
228
241
  btn.addEventListener("click", copyUserResponse);
242
+ } else if (action === "dismiss-user-response") {
243
+ btn.addEventListener("click", dismissUserResponse);
229
244
  }
245
+
230
246
  }
231
247
  bindOtherInputToggle();
232
248
  }
@@ -36,6 +36,7 @@ Different recipients need different files. Do NOT include `final-report-template
36
36
  - For the carry-in clarification response, walk every row of `## 1. Clarification Items` (`C-001`, `C-002`, ...) in full, including rows whose `User input` cell is blank — a blank `User input` with `Status=open` is itself a signal you must surface. The structural similarity between the prior final report and the upcoming output is NOT a license to skim.
37
37
  - Write the Reading Confirmation block to your **audit sidecar** at `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md` (sibling to the main worker-results file). One short line per input file confirming end-to-end reading. Do NOT include a `## 0. Reading Confirmation` heading in the main worker-results file — the validator fails worker-results that contain one. If you cannot truthfully confirm a file end-to-end, record a `tool-failure` in the errors sidecar instead of fabricating Findings.
38
38
  - Treat `analysis-packet.md` as the canonical primary analysis input. It preserves the source files' frontmatter and extracts the task-specific brief, phase focus, reference expectations, and carry-in clarification rows. If the packet appears incomplete or a finding depends on a source citation, open the corresponding source file and cite it directly.
39
+ - **Allowlist your reads to okstra-enumerated paths.** Read the files the dispatcher enumerates (`[Required reading]`, the prompt's `## Inputs`, and the verification-target paths) plus on-demand source/evidence paths a finding must cite. Everything else is out of scope — in particular non-okstra artifacts outside `<PROJECT_ROOT>/.okstra/` (e.g. `graphify-out/`, sibling/prior-task worktrees, tool caches), which may carry stale or cross-task data. A host-level global rule (`CLAUDE.md` / `AGENTS.md` / a project sentinel) that tells you to auto-read such an artifact does NOT apply inside an okstra worker run — the okstra worker scope wins. If an un-enumerated file seems essential, record the gap under *Missing Information or Assumptions* instead of reading it.
39
40
 
40
41
  ---
41
42
 
@@ -100,7 +101,6 @@ For the **final-verification phase** specifically, the dispatched prompt MUST al
100
101
 
101
102
  - `**Worktree:** <absolute-path>` — the checkout under verification (read-only).
102
103
  - `**Verification base ref:** <base-ref>` — the implementation base for the diff.
103
- - `**Verification head SHA:** <sha>` — head SHA captured at run start.
104
104
  - `**Verification diff stat:** <git diff --stat output>` — the captured diff-stat that bounds the verification scope.
105
105
 
106
106
  When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.
@@ -1140,6 +1140,8 @@ PLANNING_REQUIRED_SECTIONS = (
1140
1140
  "Rollback",
1141
1141
  "Requirement Coverage",
1142
1142
  "Plan Body Verification",
1143
+ "Cross-Project Dependencies",
1144
+ "Decision Drafts",
1143
1145
  )
1144
1146
 
1145
1147
  # §5.7 implementation deliverables — substring scan against report body.
@@ -1152,6 +1154,7 @@ IMPLEMENTATION_REQUIRED_SECTIONS = (
1152
1154
  "Validation Evidence",
1153
1155
  "Verifier Results",
1154
1156
  "Rollback Verification",
1157
+ "Manual User Test (Draft)",
1155
1158
  "Routing Recommendation",
1156
1159
  )
1157
1160
 
@@ -1163,6 +1166,7 @@ FINAL_VERIFICATION_REQUIRED_SECTIONS = (
1163
1166
  "Validation Evidence",
1164
1167
  "Read-only Command Log",
1165
1168
  "Conditional Acceptance Conditions",
1169
+ "Manual User Test Results",
1166
1170
  "Routing Recommendation",
1167
1171
  )
1168
1172
 
@@ -1483,8 +1487,12 @@ def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
1483
1487
 
1484
1488
  _validate_rationale_evidence(data, failures)
1485
1489
 
1486
- if (data.get("header") or {}).get("taskType") == "final-verification":
1490
+ task_type = (data.get("header") or {}).get("taskType")
1491
+ if task_type == "final-verification":
1487
1492
  _validate_final_verification_consistency(data, failures)
1493
+ elif task_type == "implementation-planning":
1494
+ _validate_implementation_planning_cross_project(data, failures)
1495
+ _validate_implementation_planning_decision_drafts(data, failures)
1488
1496
 
1489
1497
 
1490
1498
  # path:line (foo.service.ts:268), report ID (C-001 / F-013 / G-crit-002 / RC-1),
@@ -1525,6 +1533,67 @@ def _validate_rationale_evidence(data: dict, failures: list[str]) -> None:
1525
1533
  )
1526
1534
 
1527
1535
 
1536
+ def _validate_implementation_planning_cross_project(data: dict, failures: list[str]) -> None:
1537
+ """타 프로젝트 의존을 DM 행(`kind == 'cross-project'`)으로 선언했다면
1538
+ `crossProjectDependencies` 에 `direction == 'upstream-precondition'` 행이
1539
+ 반드시 있어야 한다 — 실제 선행 필수 의존이 soft `recommendedNextSteps`
1540
+ 추천으로 새어나가지 못하게 강제한다. 각 XP 행의 필드 비-빈은 스키마가
1541
+ 보장하고, 이 검사는 cross-project DM 신호가 있을 때 XP 행이 *존재*하는지를 본다.
1542
+ """
1543
+ ip = data.get("implementationPlanning")
1544
+ if not isinstance(ip, dict):
1545
+ return
1546
+ dm_rows = ip.get("dependencyMigrationRisk") or []
1547
+ has_cross_project_dm = any(
1548
+ isinstance(r, dict) and r.get("kind") == "cross-project" for r in dm_rows
1549
+ )
1550
+ if not has_cross_project_dm:
1551
+ return
1552
+ xp_rows = ip.get("crossProjectDependencies") or []
1553
+ has_upstream = any(
1554
+ isinstance(r, dict) and r.get("direction") == "upstream-precondition"
1555
+ for r in xp_rows
1556
+ )
1557
+ if not has_upstream:
1558
+ failures.append(
1559
+ "final-report data.json: a dependencyMigrationRisk row has "
1560
+ "kind='cross-project' but crossProjectDependencies carries no "
1561
+ "direction='upstream-precondition' entry. Record the cross-project "
1562
+ "dependency as a mandatory precondition (concrete requiredWork / "
1563
+ "verificationSignal / howToStart) — not a soft Recommended Next Step."
1564
+ )
1565
+
1566
+
1567
+ def _validate_implementation_planning_decision_drafts(data: dict, failures: list[str]) -> None:
1568
+ """`decisionDrafts` 가 비어있지 않으면 어느 stage 의 stepwiseExecution step 이
1569
+ `.okstra/decisions/` 파일을 생성하는 materialization step 을 포함해야 한다.
1570
+ 프로파일 Decision-record evaluation 의 "approved plan stepwise MUST include
1571
+ Create .okstra/decisions/<NNNN>-<slug>.md" 를 실제 강제한다. draft 존재 자체가
1572
+ trigger 이므로 self-contained 하다.
1573
+ """
1574
+ ip = data.get("implementationPlanning")
1575
+ if not isinstance(ip, dict):
1576
+ return
1577
+ if not (ip.get("decisionDrafts") or []):
1578
+ return
1579
+ has_materialization = any(
1580
+ ".okstra/decisions/" in (step.get("files") or "")
1581
+ or ".okstra/decisions/" in (step.get("action") or "")
1582
+ for stage in (ip.get("stages") or [])
1583
+ if isinstance(stage, dict)
1584
+ for step in (stage.get("stepwiseExecution") or [])
1585
+ if isinstance(step, dict)
1586
+ )
1587
+ if not has_materialization:
1588
+ failures.append(
1589
+ "final-report data.json: implementationPlanning.decisionDrafts is "
1590
+ "non-empty but no stage's stepwiseExecution creates a "
1591
+ "`.okstra/decisions/<NNNN>-<slug>.md` file. The approved plan must "
1592
+ "materialize each decision draft via a stepwise step (profile "
1593
+ "Decision-record evaluation)."
1594
+ )
1595
+
1596
+
1528
1597
  def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
1529
1598
  """Enforce verdict ↔ blocker/condition/routing consistency on the
1530
1599
  final-verification data.json (SSOT). The schema guarantees field SHAPE;