okstra 0.209.6 → 0.211.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/cli.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/wizard/prompts.ko.json +7 -6
- package/runtime/python/okstra_ctl/analysis_packet.py +7 -3
- package/runtime/python/okstra_ctl/incremental_scope.py +11 -2
- package/runtime/python/okstra_ctl/manager_launch.py +2 -1
- package/runtime/python/okstra_ctl/manager_paths.py +4 -0
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +5 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +5 -4
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/report.py +66 -19
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +8 -7
- package/runtime/python/okstra_ctl/phases/implementation/wizard.py +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +8 -3
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/report_assets/implementation-planning-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +6 -0
- package/runtime/python/okstra_ctl/plan_items.py +24 -3
- package/runtime/python/okstra_ctl/report_html/run_usage.py +9 -0
- package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -8
- package/runtime/python/okstra_ctl/report_projections.py +18 -2
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +19 -0
- package/runtime/python/okstra_ctl/report_translation.py +27 -6
- package/runtime/python/okstra_ctl/stage_close.py +13 -2
- package/runtime/python/okstra_ctl/stage_map.py +25 -0
- package/runtime/python/okstra_ctl/stage_map_cli.py +3 -2
- package/runtime/python/okstra_ctl/stage_targets.py +17 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +2 -1
- package/runtime/python/okstra_ctl/wizard/state.py +30 -1
- package/runtime/python/okstra_ctl/wizard/steps_options.py +19 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +4 -2
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +7 -1
- package/runtime/schemas/final-report-v3.0.schema.json +18 -0
- package/runtime/templates/reports/html/assets/base.css +4 -2
- package/runtime/templates/reports/html/base.template.html +14 -9
- package/runtime/templates/reports/html/i18n/en.json +91 -2
- package/runtime/templates/reports/html/i18n/ko.json +91 -2
- package/runtime/templates/reports/html/macros/forms.html +8 -7
- package/runtime/validators/validate-implementation-plan-stages.py +46 -5
|
@@ -147,6 +147,74 @@
|
|
|
147
147
|
"deferred": "Deferred",
|
|
148
148
|
"not-applicable": "Not applicable",
|
|
149
149
|
"blocked": "Blocked"
|
|
150
|
+
},
|
|
151
|
+
"clarificationKind": {
|
|
152
|
+
"material": "Missing information",
|
|
153
|
+
"decision": "Your decision",
|
|
154
|
+
"data-point": "A fact to confirm"
|
|
155
|
+
},
|
|
156
|
+
"clarificationStatus": {
|
|
157
|
+
"open": "Open",
|
|
158
|
+
"answered": "Answered",
|
|
159
|
+
"resolved": "Resolved",
|
|
160
|
+
"obsolete": "No longer relevant"
|
|
161
|
+
},
|
|
162
|
+
"approvalDisposition": {
|
|
163
|
+
"select": "Choose this option",
|
|
164
|
+
"accept-risk": "Accept the risk",
|
|
165
|
+
"request-revision": "Request a revision",
|
|
166
|
+
"reject": "Reject"
|
|
167
|
+
},
|
|
168
|
+
"approvalClassification": {
|
|
169
|
+
"user-decision": "Needs your decision",
|
|
170
|
+
"noncritical-dissent": "Non-critical disagreement",
|
|
171
|
+
"correctness-critical": "Affects correctness"
|
|
172
|
+
},
|
|
173
|
+
"clarificationScope": {
|
|
174
|
+
"in-repo": "This repository only",
|
|
175
|
+
"cross-repo": "Other repositories too",
|
|
176
|
+
"new-schema": "New schema",
|
|
177
|
+
"deferrable": "Can be deferred"
|
|
178
|
+
},
|
|
179
|
+
"executionRole": {
|
|
180
|
+
"leader": "Lead",
|
|
181
|
+
"analyser": "Analyser",
|
|
182
|
+
"critic": "Critic",
|
|
183
|
+
"designer": "Designer",
|
|
184
|
+
"implementer": "Implementer",
|
|
185
|
+
"planner": "Planner",
|
|
186
|
+
"report-writer": "Report writer",
|
|
187
|
+
"translator": "Translator",
|
|
188
|
+
"verifier": "Verifier"
|
|
189
|
+
},
|
|
190
|
+
"requirementStatus": {
|
|
191
|
+
"covered": "Covered",
|
|
192
|
+
"gap": "Not covered",
|
|
193
|
+
"documented-deviation": "Approved deviation",
|
|
194
|
+
"blocked": "Blocked"
|
|
195
|
+
},
|
|
196
|
+
"rollbackCategory": {
|
|
197
|
+
"Pure code": "Code only",
|
|
198
|
+
"Feature-flag-gated": "Behind a feature flag",
|
|
199
|
+
"Schema/config/stateful": "Schema, config or stored state"
|
|
200
|
+
},
|
|
201
|
+
"rollbackResult": {
|
|
202
|
+
"ok": "Passed",
|
|
203
|
+
"not-applicable": "Not applicable",
|
|
204
|
+
"advisory — human-run": "Advisory: a person runs it"
|
|
205
|
+
},
|
|
206
|
+
"businessFlowMode": {
|
|
207
|
+
"pre": "Before the change",
|
|
208
|
+
"contribute": "This phase's contribution",
|
|
209
|
+
"post": "After the change",
|
|
210
|
+
"unknown": "Unknown phase"
|
|
211
|
+
},
|
|
212
|
+
"businessFlowStatus": {
|
|
213
|
+
"prepared": "Prepared",
|
|
214
|
+
"running": "Running",
|
|
215
|
+
"failed": "Failed",
|
|
216
|
+
"partial": "Partly explained",
|
|
217
|
+
"complete": "Explained"
|
|
150
218
|
}
|
|
151
219
|
},
|
|
152
220
|
"enumHint": {
|
|
@@ -194,7 +262,12 @@
|
|
|
194
262
|
"agent-activity": "Agent activity",
|
|
195
263
|
"agent-activity-intro": "What each agent was asked to do in this run and how it ended. Decision cards and prose cite these rows by id.",
|
|
196
264
|
"agent": "Agent",
|
|
197
|
-
"activity": "Activity"
|
|
265
|
+
"activity": "Activity",
|
|
266
|
+
"business-flow": "Business flow explanations",
|
|
267
|
+
"business-flow-failed-count": "{count} business flow explanations failed",
|
|
268
|
+
"business-flow-failure-details": "Failure details and rerun commands",
|
|
269
|
+
"business-flow-error": "Error:",
|
|
270
|
+
"business-flow-rerun": "Rerun:"
|
|
198
271
|
},
|
|
199
272
|
"runUsage": {
|
|
200
273
|
"heading": "What this run cost",
|
|
@@ -214,7 +287,8 @@
|
|
|
214
287
|
"row-total": "Total",
|
|
215
288
|
"row-cli": "CLI calls (billed separately)",
|
|
216
289
|
"cache-read-tokens": "Cache re-reads",
|
|
217
|
-
"row-task-cumulative": "This task so far ({runs} runs)"
|
|
290
|
+
"row-task-cumulative": "This task so far ({runs} runs)",
|
|
291
|
+
"row-total-with-cli": "Total including CLI calls"
|
|
218
292
|
},
|
|
219
293
|
"macros": {
|
|
220
294
|
"forms": {
|
|
@@ -585,6 +659,21 @@
|
|
|
585
659
|
"result": "Result",
|
|
586
660
|
"no-rollback-check-was-recorded": "No rollback check was recorded.",
|
|
587
661
|
"stage-carry-over": "Stage carry-over",
|
|
662
|
+
"carry-sidecar": "Sidecar",
|
|
663
|
+
"carry-commit-range": "Commit range",
|
|
664
|
+
"carry-steps": "Steps",
|
|
665
|
+
"carry-files-changed": "Files changed",
|
|
666
|
+
"carry-raw": "Sidecar JSON as written",
|
|
667
|
+
"file": "File",
|
|
668
|
+
"change": "Change",
|
|
669
|
+
"lines": "Lines",
|
|
670
|
+
"plan-step": "Plan step",
|
|
671
|
+
"action-created": "Created",
|
|
672
|
+
"action-modified": "Modified",
|
|
673
|
+
"action-deleted": "Deleted",
|
|
674
|
+
"what-it-does": "What it does",
|
|
675
|
+
"advisory": "advisory",
|
|
676
|
+
"entry-count": "{count} entries",
|
|
588
677
|
"manual-user-test": "Manual user test",
|
|
589
678
|
"environment": "Environment:",
|
|
590
679
|
"expected": "Expected:",
|
|
@@ -147,6 +147,74 @@
|
|
|
147
147
|
"deferred": "보류",
|
|
148
148
|
"not-applicable": "해당 없음",
|
|
149
149
|
"blocked": "차단"
|
|
150
|
+
},
|
|
151
|
+
"clarificationKind": {
|
|
152
|
+
"material": "빠진 정보",
|
|
153
|
+
"decision": "결정 필요",
|
|
154
|
+
"data-point": "확인할 사실"
|
|
155
|
+
},
|
|
156
|
+
"clarificationStatus": {
|
|
157
|
+
"open": "답변 대기",
|
|
158
|
+
"answered": "답변함",
|
|
159
|
+
"resolved": "해결됨",
|
|
160
|
+
"obsolete": "더 이상 해당 없음"
|
|
161
|
+
},
|
|
162
|
+
"approvalDisposition": {
|
|
163
|
+
"select": "이 선택지 채택",
|
|
164
|
+
"accept-risk": "위험 감수",
|
|
165
|
+
"request-revision": "수정 요청",
|
|
166
|
+
"reject": "반려"
|
|
167
|
+
},
|
|
168
|
+
"approvalClassification": {
|
|
169
|
+
"user-decision": "사용자 결정 필요",
|
|
170
|
+
"noncritical-dissent": "치명적이지 않은 이견",
|
|
171
|
+
"correctness-critical": "정확성에 영향"
|
|
172
|
+
},
|
|
173
|
+
"clarificationScope": {
|
|
174
|
+
"in-repo": "이 저장소 안",
|
|
175
|
+
"cross-repo": "다른 저장소까지",
|
|
176
|
+
"new-schema": "새 스키마",
|
|
177
|
+
"deferrable": "미룰 수 있음"
|
|
178
|
+
},
|
|
179
|
+
"executionRole": {
|
|
180
|
+
"leader": "리드",
|
|
181
|
+
"analyser": "분석",
|
|
182
|
+
"critic": "비평",
|
|
183
|
+
"designer": "설계",
|
|
184
|
+
"implementer": "구현",
|
|
185
|
+
"planner": "계획",
|
|
186
|
+
"report-writer": "리포트 작성",
|
|
187
|
+
"translator": "번역",
|
|
188
|
+
"verifier": "검증"
|
|
189
|
+
},
|
|
190
|
+
"requirementStatus": {
|
|
191
|
+
"covered": "충족",
|
|
192
|
+
"gap": "미충족",
|
|
193
|
+
"documented-deviation": "승인된 이탈",
|
|
194
|
+
"blocked": "차단"
|
|
195
|
+
},
|
|
196
|
+
"rollbackCategory": {
|
|
197
|
+
"Pure code": "코드만",
|
|
198
|
+
"Feature-flag-gated": "기능 플래그로 제어",
|
|
199
|
+
"Schema/config/stateful": "스키마·설정·저장 상태"
|
|
200
|
+
},
|
|
201
|
+
"rollbackResult": {
|
|
202
|
+
"ok": "통과",
|
|
203
|
+
"not-applicable": "해당 없음",
|
|
204
|
+
"advisory — human-run": "권고: 사람이 직접 실행"
|
|
205
|
+
},
|
|
206
|
+
"businessFlowMode": {
|
|
207
|
+
"pre": "변경 전",
|
|
208
|
+
"contribute": "이번 단계 기여",
|
|
209
|
+
"post": "변경 후",
|
|
210
|
+
"unknown": "알 수 없는 단계"
|
|
211
|
+
},
|
|
212
|
+
"businessFlowStatus": {
|
|
213
|
+
"prepared": "준비됨",
|
|
214
|
+
"running": "실행 중",
|
|
215
|
+
"failed": "실패",
|
|
216
|
+
"partial": "일부 설명",
|
|
217
|
+
"complete": "설명 완료"
|
|
150
218
|
}
|
|
151
219
|
},
|
|
152
220
|
"enumHint": {
|
|
@@ -194,7 +262,12 @@
|
|
|
194
262
|
"agent-activity": "에이전트 활동",
|
|
195
263
|
"agent-activity-intro": "이번 run 에서 각 에이전트가 맡은 일과 그 결과입니다. 결정 카드와 본문이 이 행을 아이디로 인용합니다.",
|
|
196
264
|
"agent": "에이전트",
|
|
197
|
-
"activity": "활동"
|
|
265
|
+
"activity": "활동",
|
|
266
|
+
"business-flow": "업무 흐름 설명",
|
|
267
|
+
"business-flow-failed-count": "업무 흐름 설명 생성 실패 {count}건",
|
|
268
|
+
"business-flow-failure-details": "실패 내역과 재실행 명령",
|
|
269
|
+
"business-flow-error": "오류:",
|
|
270
|
+
"business-flow-rerun": "재실행:"
|
|
198
271
|
},
|
|
199
272
|
"runUsage": {
|
|
200
273
|
"heading": "이번 실행에 든 비용",
|
|
@@ -214,7 +287,8 @@
|
|
|
214
287
|
"row-total": "합계",
|
|
215
288
|
"row-cli": "CLI 호출 (별도 청구)",
|
|
216
289
|
"cache-read-tokens": "캐시 재읽기",
|
|
217
|
-
"row-task-cumulative": "이 태스크 누적 (run {runs}회)"
|
|
290
|
+
"row-task-cumulative": "이 태스크 누적 (run {runs}회)",
|
|
291
|
+
"row-total-with-cli": "CLI 호출 포함 합계"
|
|
218
292
|
},
|
|
219
293
|
"macros": {
|
|
220
294
|
"forms": {
|
|
@@ -585,6 +659,21 @@
|
|
|
585
659
|
"result": "결과",
|
|
586
660
|
"no-rollback-check-was-recorded": "롤백 확인 기록이 없습니다.",
|
|
587
661
|
"stage-carry-over": "stage 인계",
|
|
662
|
+
"carry-sidecar": "인계 파일",
|
|
663
|
+
"carry-commit-range": "commit 범위",
|
|
664
|
+
"carry-steps": "단계",
|
|
665
|
+
"carry-files-changed": "바뀐 파일 수",
|
|
666
|
+
"carry-raw": "인계 JSON 원문",
|
|
667
|
+
"file": "파일",
|
|
668
|
+
"change": "변경",
|
|
669
|
+
"lines": "줄 수",
|
|
670
|
+
"plan-step": "계획 단계",
|
|
671
|
+
"action-created": "추가",
|
|
672
|
+
"action-modified": "수정",
|
|
673
|
+
"action-deleted": "삭제",
|
|
674
|
+
"what-it-does": "하는 일",
|
|
675
|
+
"advisory": "권고",
|
|
676
|
+
"entry-count": "{count}개 항목",
|
|
588
677
|
"manual-user-test": "수동 사용자 테스트",
|
|
589
678
|
"environment": "환경:",
|
|
590
679
|
"expected": "기대 결과:",
|
|
@@ -57,18 +57,18 @@
|
|
|
57
57
|
{%- endmacro %}
|
|
58
58
|
|
|
59
59
|
{% macro clarification_article(row, compact=false) -%}
|
|
60
|
-
{% set is_closed = row.status in ['resolved', 'obsolete'] %}
|
|
60
|
+
{% set is_closed = compact or row.status in ['resolved', 'obsolete'] %}
|
|
61
61
|
{% set approval_context = row.approvalContext | default(None) %}
|
|
62
62
|
{% set resolution = row.resolution | default(None) %}
|
|
63
63
|
{% set options = row.options | default([]) %}
|
|
64
64
|
<article class="clarification-item{% if compact %} is-closed{% endif %}" id="id-{{ row.id }}" data-response-id="{{ row.id }}" data-kind="{{ row.kind }}" data-status="{{ row.status }}">
|
|
65
|
-
{% if compact %}<details><summary>{{ row.id | inline_code }} · {{ row.status }}</summary>{% endif %}
|
|
66
|
-
<p class="eyebrow">{{ row.id | inline_code }} · {{ row.kind }}</p>
|
|
65
|
+
{% if compact %}<details><summary>{{ row.id | inline_code }} · {{ row.status | enum_label('clarificationStatus') }}</summary>{% endif %}
|
|
66
|
+
<p class="eyebrow">{{ row.id | inline_code }} · {{ row.kind | enum_label('clarificationKind') }}</p>
|
|
67
67
|
{{ row.statement | paragraphs }}
|
|
68
68
|
<dl class="clarification-expected"><dt>{{ t('macros.forms.answer-as') }}</dt><dd>{{ row.expectedForm | inline_code }}</dd></dl>
|
|
69
69
|
{% if approval_context %}
|
|
70
70
|
<div class="approval-context" data-approval-classification="{{ approval_context.classification }}">
|
|
71
|
-
<p class="eyebrow">{{ approval_context.classification }}</p>
|
|
71
|
+
<p class="eyebrow">{{ approval_context.classification | enum_label('approvalClassification') }}</p>
|
|
72
72
|
<p><strong>{{ t('tasks.implementation-planning.unblock-condition') }}</strong> {{ approval_context.unblockCondition | inline_code }}</p>
|
|
73
73
|
{% set check_refs = resolution.checkRefs if resolution else approval_context.activityIds | default([]) %}
|
|
74
74
|
{% if check_refs %}<p><strong>{{ t('tasks.implementation-planning.agent-evidence') }}</strong>
|
|
@@ -81,10 +81,10 @@
|
|
|
81
81
|
{% for option in options %}
|
|
82
82
|
<li class="clarification-option{% if option.role == 'recommended' %} is-recommended{% endif %}" data-option-value="{{ option.answer }}">
|
|
83
83
|
<p class="clarification-option-answer">{{ option.answer | inline_code }}{% if option.role == 'recommended' %} <span class="badge">{{ t('macros.forms.recommended') }}</span>{% endif %}</p>
|
|
84
|
-
{% if option.disposition | default(None) %}<p class="clarification-option-disposition"
|
|
84
|
+
{% if option.disposition | default(None) %}<p class="clarification-option-disposition">{{ option.disposition | enum_label('approvalDisposition') }}</p>{% endif %}
|
|
85
85
|
<p class="clarification-option-rationale">{{ option.rationale | inline_code }}</p>
|
|
86
86
|
<dl class="clarification-option-impact">
|
|
87
|
-
<dt>{{ t('macros.forms.scope-impact') }}</dt><dd>{% if option.reach | default(None) %}{{ option.reach }}{% if option.scopeEffects | default([]) %}, {{ option.scopeEffects | join(', ') }}{% endif %}{% else %}{{ option.scopeImpact | join(', ') }}{% endif %}</dd>
|
|
87
|
+
<dt>{{ t('macros.forms.scope-impact') }}</dt><dd>{% if option.reach | default(None) %}{{ option.reach | enum_label('clarificationScope') }}{% if option.scopeEffects | default([]) %}, {{ option.scopeEffects | map('enum_label', 'clarificationScope') | join(', ') }}{% endif %}{% else %}{{ option.scopeImpact | map('enum_label', 'clarificationScope') | join(', ') }}{% endif %}</dd>
|
|
88
88
|
<dt>{{ t('macros.forms.added-work') }}</dt><dd>{{ option.addedWork | inline_code }}</dd>
|
|
89
89
|
<dt>{{ t('macros.forms.direction-change') }}</dt><dd>{{ option.directionChange | inline_code }}</dd>
|
|
90
90
|
</dl>
|
|
@@ -114,7 +114,8 @@
|
|
|
114
114
|
{% if items %}
|
|
115
115
|
{% set ns = namespace(open=[], closed=[]) %}
|
|
116
116
|
{% for row in items %}
|
|
117
|
-
{
|
|
117
|
+
{# 답이 실린 answered 행은 기다리는 질문이 아니다 — 닫힌 목록으로 간다. #}
|
|
118
|
+
{% if row.status == 'open' or (row.status == 'answered' and not row.userInput | default('')) %}
|
|
118
119
|
{% set ns.open = ns.open + [row] %}
|
|
119
120
|
{% else %}
|
|
120
121
|
{% set ns.closed = ns.closed + [row] %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""S1–
|
|
2
|
+
"""S1–S14 checks for the Stage Map structure of an approved
|
|
3
3
|
implementation-planning final-report.md. Run from prepare_task_bundle
|
|
4
4
|
of `implementation` task or standalone."""
|
|
5
5
|
|
|
@@ -35,6 +35,7 @@ from okstra_ctl.stage_map import ( # noqa: E402
|
|
|
35
35
|
parse_stage_map_text,
|
|
36
36
|
schema_v2_report,
|
|
37
37
|
stage_number_gap_message,
|
|
38
|
+
stage_row_is_cancelled,
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
HARD_STEP_CAP = 8
|
|
@@ -68,9 +69,14 @@ BARE_GIT_STATUS = re.compile(r"\bgit\b[^&|;]*?\bstatus\b[^&|;]*?--(?:porcelain|s
|
|
|
68
69
|
CLEAN_GATE_COMMAND = "okstra worktree-status --check-clean"
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
# S14 — 취소 stage 가 될 수 없는 상태. 이미 착수했거나 끝난 stage 는 통합 기준선의
|
|
73
|
+
# 일부라 취소하면 그 위에 쌓인 브랜치와 갈라진다(ADR-0015); 고치려면 max+1 보정 stage.
|
|
74
|
+
_UNCANCELLABLE_STATUSES = ("done", "active")
|
|
75
|
+
|
|
76
|
+
|
|
71
77
|
@dataclass
|
|
72
78
|
class ValidationError:
|
|
73
|
-
code: str # S1..
|
|
79
|
+
code: str # S1..S14
|
|
74
80
|
stage: int # 0 = global
|
|
75
81
|
message: str
|
|
76
82
|
|
|
@@ -380,13 +386,19 @@ def _check_conformance_declaration(
|
|
|
380
386
|
return errs
|
|
381
387
|
|
|
382
388
|
|
|
383
|
-
def _check_depends_on(
|
|
389
|
+
def _check_depends_on(
|
|
390
|
+
stages: List[StageMapStage], cancelled: frozenset[int] = frozenset(),
|
|
391
|
+
) -> List[ValidationError]:
|
|
384
392
|
errs: List[ValidationError] = []
|
|
385
393
|
valid = {s.stage_number for s in stages}
|
|
386
394
|
for s in stages:
|
|
387
395
|
for d in s.depends_on:
|
|
388
396
|
if d == s.stage_number:
|
|
389
397
|
errs.append(ValidationError("S8", s.stage_number, "self depends-on"))
|
|
398
|
+
elif d in cancelled:
|
|
399
|
+
errs.append(ValidationError("S14", s.stage_number,
|
|
400
|
+
f"depends-on {d}, which this plan cancels — a cancelled stage "
|
|
401
|
+
"is never executed; depend on the stage that replaces it"))
|
|
390
402
|
elif d not in valid:
|
|
391
403
|
errs.append(ValidationError("S6", s.stage_number,
|
|
392
404
|
f"depends-on {d} does not exist"))
|
|
@@ -495,6 +507,7 @@ def _data_stage_metas(
|
|
|
495
507
|
depends = parse_stage_dependencies(
|
|
496
508
|
str(row.get("dependsOn") or ""), row_number, ""
|
|
497
509
|
)
|
|
510
|
+
cancelled = stage_row_is_cancelled(row, row_number)
|
|
498
511
|
except StageMapError as exc:
|
|
499
512
|
errors.append(ValidationError("S2", row["stage"], exc.reason))
|
|
500
513
|
continue
|
|
@@ -504,6 +517,7 @@ def _data_stage_metas(
|
|
|
504
517
|
depends,
|
|
505
518
|
row.get("stepCount") if isinstance(row.get("stepCount"), int) else -1,
|
|
506
519
|
str(row.get("exitContractSummary") or ""),
|
|
520
|
+
cancelled,
|
|
507
521
|
))
|
|
508
522
|
return rows, errors or _stage_numbers_monotonic(rows)
|
|
509
523
|
|
|
@@ -670,6 +684,28 @@ def _check_data_stage_identities(
|
|
|
670
684
|
return errs
|
|
671
685
|
|
|
672
686
|
|
|
687
|
+
def _check_cancelled_stage_ledger(
|
|
688
|
+
stage_map: List[StageMapStage], planning: dict,
|
|
689
|
+
) -> List[ValidationError]:
|
|
690
|
+
"""S14: 원장이 이미 착수했거나 끝났다고 기록한 stage 는 취소할 수 없다."""
|
|
691
|
+
verification = planning.get("planBodyVerification")
|
|
692
|
+
ledger = (
|
|
693
|
+
verification.get("stageLedger") if isinstance(verification, dict) else None
|
|
694
|
+
)
|
|
695
|
+
if not isinstance(ledger, dict):
|
|
696
|
+
return []
|
|
697
|
+
return [
|
|
698
|
+
ValidationError("S14", meta.stage_number,
|
|
699
|
+
f"stage {meta.stage_number} is cancelled but the Stage Ledger records "
|
|
700
|
+
f"it {ledger[str(meta.stage_number)]!r}; only a ready or blocked stage "
|
|
701
|
+
"may be cancelled — keep it and add a correction stage numbered after "
|
|
702
|
+
"the highest stage instead")
|
|
703
|
+
for meta in stage_map
|
|
704
|
+
if meta.cancelled
|
|
705
|
+
and ledger.get(str(meta.stage_number)) in _UNCANCELLABLE_STATUSES
|
|
706
|
+
]
|
|
707
|
+
|
|
708
|
+
|
|
673
709
|
def collect_data_validation_errors(
|
|
674
710
|
planning: dict, user_bypassed: frozenset[int] = frozenset(),
|
|
675
711
|
) -> List[ValidationError]:
|
|
@@ -703,14 +739,19 @@ def collect_data_validation_errors(
|
|
|
703
739
|
if identity_errors:
|
|
704
740
|
return errors
|
|
705
741
|
errors.extend(_check_data_step_counts(stage_map, stages))
|
|
706
|
-
|
|
742
|
+
# 번호(S2)·대응(S3)·스텝 수는 모든 행에, 의존(S6/S8)·병렬 안전(S9)은 실행될
|
|
743
|
+
# 행에만 건다 — 취소 stage 와 그 대체 stage 는 같은 파일을 예측하기 마련이다.
|
|
744
|
+
active = [meta for meta in stage_map if not meta.cancelled]
|
|
745
|
+
cancelled = frozenset(meta.stage_number for meta in stage_map if meta.cancelled)
|
|
746
|
+
errors.extend(_check_depends_on(active, cancelled))
|
|
747
|
+
errors.extend(_check_cancelled_stage_ledger(stage_map, planning))
|
|
707
748
|
errors.extend(_report_shared_parallel_files({
|
|
708
749
|
meta.stage_number: set(PATH_TOKEN.findall(
|
|
709
750
|
str((next(
|
|
710
751
|
(s for s in stages if s.get("stage") == meta.stage_number), {}
|
|
711
752
|
)).get("exitContract") or "")
|
|
712
753
|
))
|
|
713
|
-
for meta in
|
|
754
|
+
for meta in active
|
|
714
755
|
if not meta.depends_on
|
|
715
756
|
}))
|
|
716
757
|
for stage in stages:
|