workflow-loop 0.1.0__py3-none-any.whl
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.
- workflow_loop/__init__.py +6 -0
- workflow_loop/acceptance_records.py +338 -0
- workflow_loop/artifact_paths.py +278 -0
- workflow_loop/artifact_validation.py +1738 -0
- workflow_loop/bug_record.py +203 -0
- workflow_loop/cli.py +3257 -0
- workflow_loop/data/Standardized_Repository/acceptance/acceptance.md +119 -0
- workflow_loop/data/Standardized_Repository/acceptance/acceptance_plan.md +105 -0
- workflow_loop/data/Standardized_Repository/code_design/code_design.md +204 -0
- workflow_loop/data/Standardized_Repository/code_design/project_design_init.md +152 -0
- workflow_loop/data/Standardized_Repository/code_design/revise_code_design.md +32 -0
- workflow_loop/data/Standardized_Repository/code_design/update_code_design.md +94 -0
- workflow_loop/data/Standardized_Repository/global/document_writing.md +77 -0
- workflow_loop/data/Standardized_Repository/global/workflow_lifecycle.md +91 -0
- workflow_loop/data/Standardized_Repository/impl/code_implementation.md +85 -0
- workflow_loop/data/Standardized_Repository/impl/impl.md +164 -0
- workflow_loop/data/Standardized_Repository/qa/test.md +167 -0
- workflow_loop/data/Standardized_Repository/qa/test_code.md +121 -0
- workflow_loop/data/Standardized_Repository/qa/test_code_implementation.md +67 -0
- workflow_loop/data/Standardized_Repository/qa/test_plan.md +160 -0
- workflow_loop/data/Standardized_Repository/reproduce/reproduce.md +60 -0
- workflow_loop/data/Standardized_Repository/spec/spec.md +138 -0
- workflow_loop/data/Standardized_Repository/spike/spike.md +236 -0
- workflow_loop/data/Template_Repository/acceptance/acceptance_plan.md +142 -0
- workflow_loop/data/Template_Repository/acceptance/acceptance_result.md +108 -0
- workflow_loop/data/Template_Repository/code_design/code_design.md +260 -0
- workflow_loop/data/Template_Repository/code_design/project_design_init_evidence.md +39 -0
- workflow_loop/data/Template_Repository/impl/impl.md +112 -0
- workflow_loop/data/Template_Repository/qa/test.md +102 -0
- workflow_loop/data/Template_Repository/qa/test_plan.md +100 -0
- workflow_loop/data/Template_Repository/reproduce/reproduce.md +82 -0
- workflow_loop/data/Template_Repository/spec/spec.md +222 -0
- workflow_loop/data/Template_Repository/spike/spike.md +135 -0
- workflow_loop/installer.py +632 -0
- workflow_loop/journal.py +78 -0
- workflow_loop/path_composer.py +152 -0
- workflow_loop/process_runner.py +176 -0
- workflow_loop/project.py +397 -0
- workflow_loop/role_doc.py +133 -0
- workflow_loop/rollback.py +1738 -0
- workflow_loop/spike_validation.py +379 -0
- workflow_loop/stage_materials.py +169 -0
- workflow_loop/stages/__init__.py +45 -0
- workflow_loop/stages/base.py +164 -0
- workflow_loop/stages/stages.py +1191 -0
- workflow_loop/state.py +582 -0
- workflow_loop/test_entry.py +123 -0
- workflow_loop/test_execution.py +619 -0
- workflow_loop/test_mapping.py +568 -0
- workflow_loop/test_runner.py +134 -0
- workflow_loop/topic.py +114 -0
- workflow_loop/topic_relations.py +202 -0
- workflow_loop/traceability.py +533 -0
- workflow_loop/verification.py +971 -0
- workflow_loop-0.1.0.dist-info/METADATA +187 -0
- workflow_loop-0.1.0.dist-info/RECORD +60 -0
- workflow_loop-0.1.0.dist-info/WHEEL +5 -0
- workflow_loop-0.1.0.dist-info/entry_points.txt +2 -0
- workflow_loop-0.1.0.dist-info/licenses/LICENSE +21 -0
- workflow_loop-0.1.0.dist-info/top_level.txt +1 -0
workflow_loop/cli.py
ADDED
|
@@ -0,0 +1,3257 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from . import PRODUCT_IDENTITY
|
|
9
|
+
from . import state as state_mod
|
|
10
|
+
from . import journal as journal_mod
|
|
11
|
+
from . import role_doc as role_doc_mod
|
|
12
|
+
from . import project as project_mod
|
|
13
|
+
from . import verification as verification_mod
|
|
14
|
+
from . import installer as installer_mod
|
|
15
|
+
from . import bug_record as bug_record_mod
|
|
16
|
+
from . import traceability as traceability_mod
|
|
17
|
+
from . import topic as topic_mod
|
|
18
|
+
from . import topic_relations as topic_relations_mod
|
|
19
|
+
from . import test_runner as test_runner_mod
|
|
20
|
+
from . import test_entry as test_entry_mod
|
|
21
|
+
from . import test_execution as test_execution_mod
|
|
22
|
+
from . import test_mapping as test_mapping_mod
|
|
23
|
+
from . import acceptance_records as acceptance_records_mod
|
|
24
|
+
from . import rollback as rollback_mod
|
|
25
|
+
from . import stage_materials as stage_materials_mod
|
|
26
|
+
from . import artifact_paths as artifact_paths_mod
|
|
27
|
+
from . import spike_validation as spike_validation_mod
|
|
28
|
+
from .stage_materials import MaterialError
|
|
29
|
+
from .verification import (
|
|
30
|
+
compute_code_snapshot_hash,
|
|
31
|
+
compute_non_test_code_snapshot_hash,
|
|
32
|
+
compute_test_code_snapshot_hash,
|
|
33
|
+
)
|
|
34
|
+
from .path_composer import build_stage_path, INTENT_CHOICES
|
|
35
|
+
from .stages import ProjectDesignInitStage
|
|
36
|
+
from .stages.base import StageStrategy, clean_spike_tmp
|
|
37
|
+
|
|
38
|
+
# stdout 分隔线,用于分隔"命令输出"和"下一步指令"
|
|
39
|
+
NEXT_STEP_SEPARATOR = "─" * 42
|
|
40
|
+
# .workflow_loop 目录名
|
|
41
|
+
WORKFLOW_LOOP_DIRNAME = ".workflow_loop"
|
|
42
|
+
# 所有 stage 共用的写作规范路径(相对 .workflow_loop/)
|
|
43
|
+
GLOBAL_WRITING_STANDARD_PATH = "Standardized_Repository/global/document_writing.md"
|
|
44
|
+
# from_scratch 清场时探测的过程产物目录列表(Clean Detect List)
|
|
45
|
+
# 这些目录下有文件时需要 --confirm-clean 才能删除
|
|
46
|
+
CLEAN_DETECT_DIRS = ["spec", "acceptance", "qa", "impl", "bug"]
|
|
47
|
+
# from_scratch 清场时需要一起删除的项目根文件
|
|
48
|
+
CLEAN_DETECT_FILES = [artifact_paths_mod.TRACEABILITY_DOC]
|
|
49
|
+
|
|
50
|
+
STAGE_LABELS = {
|
|
51
|
+
"spec": "产品设计",
|
|
52
|
+
"code_design": "初步代码设计",
|
|
53
|
+
"revise_code_design": "设计期代码设计修订",
|
|
54
|
+
"project_design_init": "项目设计初始化",
|
|
55
|
+
"reproduce": "缺陷复现",
|
|
56
|
+
"spike": "技术不确定性穿刺",
|
|
57
|
+
"acceptance_plan": "验收计划",
|
|
58
|
+
"test_plan": "测试计划",
|
|
59
|
+
"impl": "代码实施",
|
|
60
|
+
"test_code": "测试代码编写",
|
|
61
|
+
"test_execution": "测试执行",
|
|
62
|
+
"topic_acceptance": "主题验收",
|
|
63
|
+
"regression_test": "最终全量回归",
|
|
64
|
+
"overall_acceptance": "整体验收",
|
|
65
|
+
"update_code_design": "最终产品、架构与代码设计同步",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# 打印 stdout 末尾的"下一步"指令(stdout 驱动原则的核心)
|
|
70
|
+
# 每条命令结束前都调这个,AI 读 stdout 知道下一步干啥
|
|
71
|
+
def print_next_step(instruction: str) -> None:
|
|
72
|
+
# 分隔线 + 下一步指令
|
|
73
|
+
print(f"\n{NEXT_STEP_SEPARATOR}\n下一步:{instruction}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def stage_label(stage_name: str) -> str:
|
|
77
|
+
"""给 stage 标识补充中文含义,避免用户只看到英文代码名。"""
|
|
78
|
+
label = STAGE_LABELS.get(stage_name)
|
|
79
|
+
return f"{stage_name}({label})" if label else stage_name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def confirmation_next_step(stage_name: str) -> str:
|
|
83
|
+
"""第二道门通过后,用用户真正需要判断的问题说明第三道门。"""
|
|
84
|
+
command = f"`workflow gate {stage_name} --confirmed`"
|
|
85
|
+
if stage_name == "regression_test":
|
|
86
|
+
return (
|
|
87
|
+
"把刚才程序真实执行的全量测试命令、退出码和输出摘要交给用户查看,"
|
|
88
|
+
f"问“这条实际结果是否可用于继续”;用户确认后由 AI 执行 {command}"
|
|
89
|
+
)
|
|
90
|
+
if stage_name == "overall_acceptance":
|
|
91
|
+
return (
|
|
92
|
+
"把全部主题验收结果和最终全量回归结果交给用户查看,"
|
|
93
|
+
f"问“全部主题组合后是否已经完成这次需求”;用户确认后由 AI 执行 {command}"
|
|
94
|
+
)
|
|
95
|
+
if stage_name == "update_code_design":
|
|
96
|
+
return (
|
|
97
|
+
"把最终产品说明、代码架构设计和真实代码核对结果交给用户查看;"
|
|
98
|
+
f"用户确认后由 AI 执行 {command},随后立即执行 `workflow done`(正式收工),"
|
|
99
|
+
"不再重复询问"
|
|
100
|
+
)
|
|
101
|
+
return (
|
|
102
|
+
f"把当前 {stage_label(stage_name)}的程序检查结果和产出交给用户查看;"
|
|
103
|
+
f"用户明确同意后由 AI 执行 {command}(第三道门:记录用户确认并进入下一环节)"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def recovery_instruction(wf_state) -> str | None:
|
|
108
|
+
"""返回当前阶段在恢复流程中的动作说明。"""
|
|
109
|
+
summary = verification_mod.recovery_summary(wf_state)
|
|
110
|
+
action = verification_mod.recovery_stage_action(wf_state, wf_state.current_stage)
|
|
111
|
+
if not summary or not action:
|
|
112
|
+
return None
|
|
113
|
+
return f"原因:{summary}。当前不是从头重做,当前阶段要做的是:{action}。"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def print_recovery_details(wf_state) -> None:
|
|
117
|
+
"""打印当前退回原因和本阶段动作;没有恢复上下文时不输出。"""
|
|
118
|
+
summary = verification_mod.recovery_summary(wf_state)
|
|
119
|
+
action = verification_mod.recovery_stage_action(wf_state, wf_state.current_stage)
|
|
120
|
+
if not summary or not action:
|
|
121
|
+
return
|
|
122
|
+
print(f"退回原因: {summary}")
|
|
123
|
+
print(f"当前阶段: {stage_label(wf_state.current_stage)}")
|
|
124
|
+
print(f"当前要做: {action}")
|
|
125
|
+
print("说明: 重新经过一个阶段不等于从头重做;先核对旧产出,只有不再符合最新上游时才修改。")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# 根据当前阶段的门禁状态,给出不会跨阶段的下一步
|
|
129
|
+
def current_stage_next_instruction(wf_state) -> str:
|
|
130
|
+
stage_name = wf_state.current_stage
|
|
131
|
+
if stage_name == "completed":
|
|
132
|
+
return "调 `workflow done` 标记本次工作流完成"
|
|
133
|
+
|
|
134
|
+
stage_state = wf_state.stages.get(stage_name)
|
|
135
|
+
if stage_state is None:
|
|
136
|
+
return "调 `workflow status` 查看当前工作流状态"
|
|
137
|
+
|
|
138
|
+
gate = stage_state.gate
|
|
139
|
+
recovery = recovery_instruction(wf_state)
|
|
140
|
+
prefix = f"{recovery} " if recovery else ""
|
|
141
|
+
if not gate.discussion_complete:
|
|
142
|
+
return (
|
|
143
|
+
f"{prefix}调 `workflow discuss` 加载当前 {stage_label(stage_name)}材料;"
|
|
144
|
+
f"讨论完成后调 `workflow gate {stage_name} --discuss-done`"
|
|
145
|
+
"(第一道门:只记录当前问题已经聊清楚,可以开始产出)"
|
|
146
|
+
)
|
|
147
|
+
if not gate.code_validated:
|
|
148
|
+
if stage_name == "topic_acceptance":
|
|
149
|
+
project_root = resolve_project_root() or os.getcwd()
|
|
150
|
+
progress = acceptance_records_mod.acceptance_progress(project_root, wf_state)
|
|
151
|
+
pending = [line for line in progress if "待验收" in line]
|
|
152
|
+
if pending:
|
|
153
|
+
return (
|
|
154
|
+
f"{prefix}继续按主题逐条验收;用户回答后调 `workflow acceptance record`。"
|
|
155
|
+
f"当前待处理:{pending}"
|
|
156
|
+
)
|
|
157
|
+
return (
|
|
158
|
+
f"{prefix}生成或复核全部 `acceptance/<主题文件标识>_验收结果.md` 后,"
|
|
159
|
+
"调 `workflow gate topic_acceptance`"
|
|
160
|
+
)
|
|
161
|
+
if stage_name == "impl" and recovery:
|
|
162
|
+
if stage_state.existing_code_accepted_hash is not None:
|
|
163
|
+
return f"{prefix}既有实施代码已经确认,调 `workflow gate impl` 执行实施校验"
|
|
164
|
+
return (
|
|
165
|
+
f"{prefix}如果现有代码已经符合最新计划,先调 "
|
|
166
|
+
"`workflow gate impl --accept-existing-code`;否则修改代码后调 `workflow gate impl`"
|
|
167
|
+
)
|
|
168
|
+
if stage_name == "impl":
|
|
169
|
+
prepared, _, _ = rollback_mod.validate_prepared(
|
|
170
|
+
resolve_project_root() or os.getcwd(),
|
|
171
|
+
wf_state,
|
|
172
|
+
)
|
|
173
|
+
if not prepared:
|
|
174
|
+
return (
|
|
175
|
+
f"{prefix}先调 `workflow gate impl --prepare-code` 保存实施计划所列文件的修改前内容;"
|
|
176
|
+
"保存成功后再修改代码"
|
|
177
|
+
)
|
|
178
|
+
if stage_name == "test_code" and recovery:
|
|
179
|
+
if stage_state.existing_test_code_accepted_hash is not None:
|
|
180
|
+
return f"{prefix}既有测试代码已经确认,调 `workflow gate test_code` 执行测试代码校验"
|
|
181
|
+
return (
|
|
182
|
+
f"{prefix}如果现有测试代码已经覆盖最新测试计划,先调 "
|
|
183
|
+
"`workflow gate test_code --accept-existing-test-code`;否则修改测试代码后调 `workflow gate test_code`"
|
|
184
|
+
)
|
|
185
|
+
return (
|
|
186
|
+
f"{prefix}完成当前 {stage_label(stage_name)}的产出文件后,"
|
|
187
|
+
f"调 `workflow gate {stage_name}`(不带选项,第二道门:程序检查固定事实)"
|
|
188
|
+
)
|
|
189
|
+
if not gate.user_confirmed:
|
|
190
|
+
return f"{prefix}{confirmation_next_step(stage_name)}"
|
|
191
|
+
return "调 `workflow status` 查看当前工作流状态"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def restore_recovery_context_from_journal(project_root: str, wf_state) -> bool:
|
|
195
|
+
"""为旧 state.json 从 Journal(追加式历史日志)补回退回说明。"""
|
|
196
|
+
if wf_state.recovery.source_stage:
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
journal_entries = journal_mod.read_all(project_root)
|
|
200
|
+
handled_recovery_ids = {
|
|
201
|
+
entry.get("recovery_created_at")
|
|
202
|
+
for entry in journal_entries
|
|
203
|
+
if entry.get("action") == "恢复提示已处理"
|
|
204
|
+
and entry.get("recovery_created_at")
|
|
205
|
+
and entry.get("workflow_id") in (None, wf_state.workflow_id)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
first_affected_stage = {
|
|
209
|
+
"acceptance_plan": "acceptance_plan",
|
|
210
|
+
"test_plan": "test_plan",
|
|
211
|
+
"impl": "impl",
|
|
212
|
+
"test_code": "test_code",
|
|
213
|
+
"test_execution": "topic_acceptance",
|
|
214
|
+
"topic_acceptance": "regression_test",
|
|
215
|
+
"regression_test": "regression_test",
|
|
216
|
+
}
|
|
217
|
+
default_reasons = {
|
|
218
|
+
"acceptance_plan": "验收主题或验收条件已经改变,后续计划、代码和结果必须重新核对",
|
|
219
|
+
"test_plan": "测试项、测试方式或测试范围已经改变,后续实施和测试必须重新核对",
|
|
220
|
+
"impl": "实施代码或实施记录已经改变,原测试和验收结果不能继续代表当前实现",
|
|
221
|
+
"test_code": "测试代码、测试配置或统一测试入口已经改变,旧执行记录必须作废",
|
|
222
|
+
"test_execution": "主题测试结果已经改变,旧主题验收和后续结论必须重新确认",
|
|
223
|
+
"topic_acceptance": "主题验收结果已经改变,旧全量回归和整体验收结论不能继续使用",
|
|
224
|
+
"regression_test": "全量回归状态或回归后的代码已经改变,必须重新执行全量回归",
|
|
225
|
+
}
|
|
226
|
+
stage_indexes = {name: index for index, name in enumerate(wf_state.stage_path)}
|
|
227
|
+
current_index = stage_indexes.get(wf_state.current_stage)
|
|
228
|
+
if current_index is None:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
for entry in reversed(journal_entries):
|
|
232
|
+
entry_workflow_id = entry.get("workflow_id")
|
|
233
|
+
if entry_workflow_id not in (None, wf_state.workflow_id):
|
|
234
|
+
continue
|
|
235
|
+
action = entry.get("action")
|
|
236
|
+
if action == "验证失效":
|
|
237
|
+
source_stage = entry.get("from_stage")
|
|
238
|
+
target_stage = first_affected_stage.get(source_stage)
|
|
239
|
+
reason = entry.get("reason")
|
|
240
|
+
if reason in {None, "上游内容已变化", "用户确认前发现上游内容已变化"}:
|
|
241
|
+
reason = default_reasons.get(source_stage, "上游内容变化")
|
|
242
|
+
elif action == "流程退回":
|
|
243
|
+
source_stage = entry.get("to_stage")
|
|
244
|
+
target_stage = source_stage
|
|
245
|
+
reason = entry.get("reason") or "用户确认退回"
|
|
246
|
+
else:
|
|
247
|
+
continue
|
|
248
|
+
recovery_created_at = entry.get("recovery_created_at")
|
|
249
|
+
if recovery_created_at and recovery_created_at in handled_recovery_ids:
|
|
250
|
+
continue
|
|
251
|
+
if not source_stage or target_stage not in stage_indexes:
|
|
252
|
+
continue
|
|
253
|
+
target_index = stage_indexes[target_stage]
|
|
254
|
+
if current_index < target_index:
|
|
255
|
+
continue
|
|
256
|
+
# 兼容没有 recovery_created_at 的旧 Journal:源阶段已经完成时,
|
|
257
|
+
# 说明这条历史原因已经处理过,不再重新恢复为当前提示。
|
|
258
|
+
source_state = wf_state.stages.get(source_stage)
|
|
259
|
+
if not recovery_created_at and source_state is not None and source_state.status == "done":
|
|
260
|
+
continue
|
|
261
|
+
affected_stages = wf_state.stage_path[target_index:]
|
|
262
|
+
if wf_state.current_stage not in affected_stages:
|
|
263
|
+
continue
|
|
264
|
+
verification_mod.set_recovery_context(
|
|
265
|
+
wf_state,
|
|
266
|
+
source_stage,
|
|
267
|
+
affected_stages,
|
|
268
|
+
reason,
|
|
269
|
+
)
|
|
270
|
+
return True
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def clear_completed_material_recovery(project_root: str, wf_state) -> bool:
|
|
275
|
+
"""清除已经完成的恢复提示,并保留带关联标识的 Journal 历史。"""
|
|
276
|
+
recovery = wf_state.recovery
|
|
277
|
+
if not verification_mod.clear_completed_material_recovery(wf_state):
|
|
278
|
+
return False
|
|
279
|
+
journal_mod.append_entry(
|
|
280
|
+
project_root,
|
|
281
|
+
"恢复提示已处理",
|
|
282
|
+
"workflow.py",
|
|
283
|
+
workflow_id=wf_state.workflow_id,
|
|
284
|
+
source_stage=recovery.source_stage,
|
|
285
|
+
reason=recovery.reason,
|
|
286
|
+
recovery_created_at=recovery.created_at,
|
|
287
|
+
)
|
|
288
|
+
state_mod.save_state(project_root, wf_state)
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def ensure_impl_recovery_baseline(project_root: str, wf_state) -> bool:
|
|
293
|
+
"""恢复到 impl 时记录当前代码,供后续判断是复用还是重新实施。"""
|
|
294
|
+
if wf_state.current_stage != "impl":
|
|
295
|
+
return False
|
|
296
|
+
if not verification_mod.recovery_summary(wf_state):
|
|
297
|
+
return False
|
|
298
|
+
stage_state = wf_state.stages.get("impl")
|
|
299
|
+
if stage_state is None or stage_state.code_baseline_hash is not None:
|
|
300
|
+
return False
|
|
301
|
+
stage_state.code_baseline_hash = compute_non_test_code_snapshot_hash(project_root)
|
|
302
|
+
journal_mod.append_entry(
|
|
303
|
+
project_root,
|
|
304
|
+
"恢复流程实施代码基线",
|
|
305
|
+
"workflow.py",
|
|
306
|
+
workflow_id=wf_state.workflow_id,
|
|
307
|
+
stage="impl",
|
|
308
|
+
code_snapshot_hash=stage_state.code_baseline_hash,
|
|
309
|
+
reason="恢复流程开始时记录现有代码,后续由用户决定复用还是修改",
|
|
310
|
+
)
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# 从当前工作目录向上查找 .workflow_loop/ 目录,定位项目根
|
|
315
|
+
# 日常命令(start/discuss/gate 等)用这个找项目根
|
|
316
|
+
# 安装命令(_install-project)不用这个,直接用 cwd
|
|
317
|
+
def resolve_project_root() -> str | None:
|
|
318
|
+
# 从当前目录开始
|
|
319
|
+
current = os.getcwd()
|
|
320
|
+
# 一直向上找
|
|
321
|
+
while True:
|
|
322
|
+
# 找到 .workflow_loop/ → 返回当前目录作为项目根
|
|
323
|
+
if os.path.exists(os.path.join(current, WORKFLOW_LOOP_DIRNAME)):
|
|
324
|
+
return current
|
|
325
|
+
# 取父目录
|
|
326
|
+
parent = os.path.dirname(current)
|
|
327
|
+
# 到根目录了还没找到 → 返回 None
|
|
328
|
+
if parent == current:
|
|
329
|
+
return None
|
|
330
|
+
# 继续向上
|
|
331
|
+
current = parent
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def compute_stage_material_hash(project_root: str, stage: StageStrategy) -> str:
|
|
335
|
+
"""计算当前阶段材料清单的内容指纹。
|
|
336
|
+
|
|
337
|
+
指纹覆盖阶段模板、工作规范、全局写作规范、角色说明、内置阶段任务和附加材料;
|
|
338
|
+
材料缺失、不是普通文件或不可读时抛 MaterialError,调用方不得登记材料记录。
|
|
339
|
+
"""
|
|
340
|
+
return stage_materials_mod.compute_fingerprint(
|
|
341
|
+
project_root,
|
|
342
|
+
stage.name(),
|
|
343
|
+
role_doc_mod.get_role_doc(stage.name()),
|
|
344
|
+
stage.instruction(),
|
|
345
|
+
stage.materials(),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# 记录进入穿刺阶段时的产品设计和代码设计内容哈希
|
|
350
|
+
# 新流程只在真正进入 spike 时记录;旧 state.json 已经停在 spike 且没有基线时,明确标记无法还原
|
|
351
|
+
# 旧状态里的穿刺产物路径也在这里迁移为新的固定清单路径
|
|
352
|
+
def ensure_spike_baseline(
|
|
353
|
+
project_root: str,
|
|
354
|
+
wf_state: state_mod.WorkflowState,
|
|
355
|
+
*,
|
|
356
|
+
capture_if_missing: bool = False,
|
|
357
|
+
) -> bool:
|
|
358
|
+
changed = False
|
|
359
|
+
spike_state = wf_state.stages.get("spike")
|
|
360
|
+
expected_artifact_paths = [artifact_paths_mod.SPIKE_INDEX_DOC]
|
|
361
|
+
if spike_state is not None and spike_state.artifact_paths != expected_artifact_paths:
|
|
362
|
+
spike_state.artifact_paths = expected_artifact_paths
|
|
363
|
+
changed = True
|
|
364
|
+
journal_mod.append_entry(
|
|
365
|
+
project_root,
|
|
366
|
+
"穿刺状态迁移",
|
|
367
|
+
"workflow.py",
|
|
368
|
+
artifact_paths=expected_artifact_paths,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if wf_state.spike_baseline.captured_at is not None:
|
|
372
|
+
return changed
|
|
373
|
+
|
|
374
|
+
if not capture_if_missing:
|
|
375
|
+
if not wf_state.spike_baseline.legacy_unavailable:
|
|
376
|
+
wf_state.spike_baseline.legacy_unavailable = True
|
|
377
|
+
journal_mod.append_entry(
|
|
378
|
+
project_root,
|
|
379
|
+
"穿刺基线缺失",
|
|
380
|
+
"workflow.py",
|
|
381
|
+
reason="旧工作流没有保存进入 spike 时的设计哈希,不能用当前文件冒充旧基线",
|
|
382
|
+
)
|
|
383
|
+
changed = True
|
|
384
|
+
return changed
|
|
385
|
+
|
|
386
|
+
product_hash, product_paths = verification_mod.compute_product_design_hash(project_root)
|
|
387
|
+
wf_state.spike_baseline = state_mod.SpikeBaselineState(
|
|
388
|
+
captured_at=state_mod.now_iso(),
|
|
389
|
+
product_design_hash=product_hash,
|
|
390
|
+
product_design_paths=product_paths,
|
|
391
|
+
code_design_hash=verification_mod.compute_code_design_hash(project_root),
|
|
392
|
+
legacy_unavailable=False,
|
|
393
|
+
)
|
|
394
|
+
journal_mod.append_entry(
|
|
395
|
+
project_root,
|
|
396
|
+
"穿刺设计基线",
|
|
397
|
+
"workflow.py",
|
|
398
|
+
product_design_hash=product_hash,
|
|
399
|
+
product_design_paths=product_paths,
|
|
400
|
+
code_design_hash=wf_state.spike_baseline.code_design_hash,
|
|
401
|
+
)
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def ensure_stage_artifact_baseline(
|
|
406
|
+
project_root: str,
|
|
407
|
+
wf_state: state_mod.WorkflowState,
|
|
408
|
+
stage: StageStrategy,
|
|
409
|
+
) -> bool:
|
|
410
|
+
"""在开始写产物前保存文件哈希;同一阶段只记录一次。"""
|
|
411
|
+
stage_state = wf_state.stages.get(stage.name())
|
|
412
|
+
if stage_state is None or stage_state.artifact_baseline_captured_at is not None:
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
tracked_paths = stage.change_tracked_paths(project_root)
|
|
416
|
+
if not tracked_paths:
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
stage_state.artifact_baseline_captured_at = state_mod.now_iso()
|
|
420
|
+
stage_state.artifact_baseline_hashes = verification_mod.compute_file_hashes(
|
|
421
|
+
project_root,
|
|
422
|
+
tracked_paths,
|
|
423
|
+
)
|
|
424
|
+
journal_mod.append_entry(
|
|
425
|
+
project_root,
|
|
426
|
+
"阶段产物基线",
|
|
427
|
+
"workflow.py",
|
|
428
|
+
stage=stage.name(),
|
|
429
|
+
artifact_hashes=stage_state.artifact_baseline_hashes,
|
|
430
|
+
)
|
|
431
|
+
return True
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def ensure_stage_path_current(project_root: str, wf_state: state_mod.WorkflowState) -> bool:
|
|
435
|
+
"""让当前状态中的阶段路径和现行阶段定义保持一致。"""
|
|
436
|
+
stage_instances = build_stage_path(wf_state.intent, project_root)
|
|
437
|
+
|
|
438
|
+
# project_design_init(项目设计初始化)只在开工时决定是否加入路径。
|
|
439
|
+
# 本次 Run 已经包含它时,即使项目标记后来变为 true,也要保留本次路径。
|
|
440
|
+
if "project_design_init" in wf_state.stage_path and not any(
|
|
441
|
+
stage.name() == "project_design_init" for stage in stage_instances
|
|
442
|
+
):
|
|
443
|
+
stage_instances.insert(0, ProjectDesignInitStage())
|
|
444
|
+
|
|
445
|
+
expected_names = [stage.name() for stage in stage_instances]
|
|
446
|
+
if wf_state.stage_path == expected_names:
|
|
447
|
+
artifact_paths_changed = False
|
|
448
|
+
for stage in stage_instances:
|
|
449
|
+
stage_state = wf_state.stages.get(stage.name())
|
|
450
|
+
expected_artifacts = stage.artifact_paths()
|
|
451
|
+
if stage_state is not None and stage_state.artifact_paths != expected_artifacts:
|
|
452
|
+
stage_state.artifact_paths = expected_artifacts
|
|
453
|
+
artifact_paths_changed = True
|
|
454
|
+
if artifact_paths_changed:
|
|
455
|
+
journal_mod.append_entry(
|
|
456
|
+
project_root,
|
|
457
|
+
"阶段产物路径迁移",
|
|
458
|
+
"workflow.py",
|
|
459
|
+
stage_path=expected_names,
|
|
460
|
+
)
|
|
461
|
+
return artifact_paths_changed
|
|
462
|
+
|
|
463
|
+
old_stages = wf_state.stages
|
|
464
|
+
new_stages = {}
|
|
465
|
+
for stage in stage_instances:
|
|
466
|
+
stage_name = stage.name()
|
|
467
|
+
if stage_name in old_stages:
|
|
468
|
+
stage_state = old_stages[stage_name]
|
|
469
|
+
stage_state.artifact_paths = stage.artifact_paths()
|
|
470
|
+
else:
|
|
471
|
+
stage_state = state_mod.StageState(
|
|
472
|
+
status="pending",
|
|
473
|
+
artifact_paths=stage.artifact_paths(),
|
|
474
|
+
artifact_produced_at=None,
|
|
475
|
+
gate=state_mod.GateState(),
|
|
476
|
+
)
|
|
477
|
+
new_stages[stage_name] = stage_state
|
|
478
|
+
|
|
479
|
+
current_stage = "completed"
|
|
480
|
+
for stage_name in expected_names:
|
|
481
|
+
if new_stages[stage_name].status != "done":
|
|
482
|
+
current_stage = stage_name
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
for stage_name, stage_state in new_stages.items():
|
|
486
|
+
if stage_state.status != "done":
|
|
487
|
+
stage_state.status = "in_progress" if stage_name == current_stage else "pending"
|
|
488
|
+
|
|
489
|
+
previous_path = wf_state.stage_path
|
|
490
|
+
previous_stage = wf_state.current_stage
|
|
491
|
+
wf_state.stage_path = expected_names
|
|
492
|
+
wf_state.stages = new_stages
|
|
493
|
+
wf_state.current_stage = current_stage
|
|
494
|
+
journal_mod.append_entry(
|
|
495
|
+
project_root,
|
|
496
|
+
"阶段路径迁移",
|
|
497
|
+
"workflow.py",
|
|
498
|
+
previous_path=previous_path,
|
|
499
|
+
current_path=expected_names,
|
|
500
|
+
previous_stage=previous_stage,
|
|
501
|
+
current_stage=current_stage,
|
|
502
|
+
)
|
|
503
|
+
return True
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# 从 stage 实例列表里找对应 stage 名的策略实例
|
|
507
|
+
# discuss 和 gate 命令用这个找当前 stage 的策略
|
|
508
|
+
def get_stage_strategy(stage_name: str, state: state_mod.WorkflowState, stage_instances: list[StageStrategy]) -> StageStrategy | None:
|
|
509
|
+
# 遍历 stage 实例列表
|
|
510
|
+
for stage in stage_instances:
|
|
511
|
+
# 名字匹配 → 返回该实例
|
|
512
|
+
if stage.name() == stage_name:
|
|
513
|
+
return stage
|
|
514
|
+
# 没找到
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# 探测项目根下是否有过程产物(from_scratch 清场用)
|
|
519
|
+
# 检查 CLEAN_DETECT_DIRS 里的目录是否存在且含文件
|
|
520
|
+
# 不监测 .workflow_loop/Template_Repository/ 和 Standardized_Repository/
|
|
521
|
+
def detect_clean_artifacts(project_root: str) -> list[str]:
|
|
522
|
+
# 收集有内容的产物目录
|
|
523
|
+
found = []
|
|
524
|
+
# 遍历清场监测清单
|
|
525
|
+
for dir_name in CLEAN_DETECT_DIRS:
|
|
526
|
+
# 拼出目录路径
|
|
527
|
+
dir_path = os.path.join(project_root, dir_name)
|
|
528
|
+
# 目录存在
|
|
529
|
+
if os.path.isdir(dir_path):
|
|
530
|
+
# 检查目录下是否有任何文件(递归遍历)
|
|
531
|
+
has_files = any(
|
|
532
|
+
os.path.isfile(os.path.join(root, f))
|
|
533
|
+
for root, dirs, files in os.walk(dir_path)
|
|
534
|
+
for f in files
|
|
535
|
+
)
|
|
536
|
+
# 有文件 → 加入待删清单
|
|
537
|
+
if has_files:
|
|
538
|
+
found.append(dir_name)
|
|
539
|
+
for file_name in CLEAN_DETECT_FILES:
|
|
540
|
+
if os.path.isfile(os.path.join(project_root, file_name)):
|
|
541
|
+
found.append(file_name)
|
|
542
|
+
# 返回有内容的目录列表
|
|
543
|
+
return found
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# 删除项目根下的过程产物(from_scratch --confirm-clean 时调用)
|
|
547
|
+
# 删除 CLEAN_DETECT_DIRS 里有文件的目录
|
|
548
|
+
def clean_artifacts(project_root: str) -> list[str]:
|
|
549
|
+
# 收集已清理的目录
|
|
550
|
+
cleaned = []
|
|
551
|
+
# 遍历清场监测清单
|
|
552
|
+
for dir_name in CLEAN_DETECT_DIRS:
|
|
553
|
+
# 拼出目录路径
|
|
554
|
+
dir_path = os.path.join(project_root, dir_name)
|
|
555
|
+
# 目录存在
|
|
556
|
+
if os.path.isdir(dir_path):
|
|
557
|
+
# 检查是否有文件
|
|
558
|
+
has_files = any(
|
|
559
|
+
os.path.isfile(os.path.join(root, f))
|
|
560
|
+
for root, dirs, files in os.walk(dir_path)
|
|
561
|
+
for f in files
|
|
562
|
+
)
|
|
563
|
+
# 有文件 → 删除整个目录
|
|
564
|
+
if has_files:
|
|
565
|
+
shutil.rmtree(dir_path)
|
|
566
|
+
cleaned.append(dir_name)
|
|
567
|
+
for file_name in CLEAN_DETECT_FILES:
|
|
568
|
+
file_path = os.path.join(project_root, file_name)
|
|
569
|
+
if os.path.isfile(file_path):
|
|
570
|
+
os.remove(file_path)
|
|
571
|
+
cleaned.append(file_name)
|
|
572
|
+
# 返回已清理的目录列表
|
|
573
|
+
return cleaned
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# start 命令:启动工作流或检查状态
|
|
577
|
+
# 不带 --intent → 只读状态检查,不初始化 Run、不写任何文件
|
|
578
|
+
# 带 --intent → 初始化 Run(from_scratch 另循 Clean Confirm 与清场开工事务)
|
|
579
|
+
INTENT_LABELS = {
|
|
580
|
+
"from_scratch": "从零做:几乎空着手交付新能力或新项目",
|
|
581
|
+
"product_change": "改产品:在已有产品上修改设计或增加功能",
|
|
582
|
+
"bugfix": "修 bug:定位并修复一个具体缺陷",
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def refuse_if_pending_start_transaction(project_root: str) -> None:
|
|
587
|
+
"""未完成的清场开工事务存在时,任何日常命令都不得继续正常流程。"""
|
|
588
|
+
transaction = rollback_mod.read_start_transaction(project_root)
|
|
589
|
+
if transaction is None:
|
|
590
|
+
return
|
|
591
|
+
print("错误:发现未完成的清场开工事务,项目可能处于清场到一半的状态。")
|
|
592
|
+
print_next_step(
|
|
593
|
+
"由 AI 执行 `workflow start`(开工检查):程序会按事务记录先把项目恢复到开工前状态,"
|
|
594
|
+
"恢复成功后才能继续正常流程"
|
|
595
|
+
)
|
|
596
|
+
sys.exit(1)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _restore_start_failure(project_root: str, workflow_id: str, manifest: dict) -> list[str]:
|
|
600
|
+
"""开工事务中任一步失败后,恢复受管文档、项目字段和开工前状态。
|
|
601
|
+
|
|
602
|
+
返回未能恢复的说明列表;为空表示恢复完整(此时清理事务记录和本轮副本)。
|
|
603
|
+
"""
|
|
604
|
+
restored, failures = rollback_mod.restore_start_baseline(project_root, workflow_id)
|
|
605
|
+
try:
|
|
606
|
+
project_mod.restore_managed_fields(
|
|
607
|
+
project_root,
|
|
608
|
+
manifest.get("project_fields") or {},
|
|
609
|
+
)
|
|
610
|
+
except (OSError, ValueError) as exc:
|
|
611
|
+
failures.append(f"project.json 项目字段({exc})")
|
|
612
|
+
|
|
613
|
+
state_path = os.path.join(project_root, WORKFLOW_LOOP_DIRNAME, "state.json")
|
|
614
|
+
previous_state_raw = manifest.get("previous_state_raw")
|
|
615
|
+
try:
|
|
616
|
+
if previous_state_raw is None:
|
|
617
|
+
if os.path.exists(state_path):
|
|
618
|
+
os.remove(state_path)
|
|
619
|
+
else:
|
|
620
|
+
with open(state_path, "w", encoding="utf-8") as stream:
|
|
621
|
+
stream.write(previous_state_raw)
|
|
622
|
+
except (OSError, ValueError) as exc:
|
|
623
|
+
failures.append(f"state.json({exc})")
|
|
624
|
+
|
|
625
|
+
if not failures:
|
|
626
|
+
rollback_mod.clear_start_transaction(project_root)
|
|
627
|
+
rollback_mod.cleanup(project_root, workflow_id)
|
|
628
|
+
return failures
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def handle_pending_start_transaction(project_root: str) -> bool:
|
|
632
|
+
"""workflow start 先识别并处理未完成的开工事务;返回是否可以继续。"""
|
|
633
|
+
transaction = rollback_mod.read_start_transaction(project_root)
|
|
634
|
+
if transaction is None:
|
|
635
|
+
return True
|
|
636
|
+
if transaction.get("status") == "committed":
|
|
637
|
+
# 上次开工已成功,只是事务记录没来得及删除 → 只完成清理,不回退已经启动的 Run
|
|
638
|
+
rollback_mod.clear_start_transaction(project_root)
|
|
639
|
+
print("上一次开工事务已经成功,只清理了遗留的事务记录。")
|
|
640
|
+
return True
|
|
641
|
+
workflow_id = transaction.get("workflow_id")
|
|
642
|
+
print("发现未完成的清场开工事务,先把项目恢复到开工前状态...")
|
|
643
|
+
manifest = rollback_mod.read_start_baseline(project_root, workflow_id or "") or {}
|
|
644
|
+
failures = _restore_start_failure(project_root, workflow_id or "", manifest)
|
|
645
|
+
if failures:
|
|
646
|
+
print("恢复不完整,以下内容未恢复(事务记录和副本已保留):")
|
|
647
|
+
for item in failures:
|
|
648
|
+
print(f" - {item}")
|
|
649
|
+
print_next_step("先人工检查上述路径,再重新执行 `workflow start`")
|
|
650
|
+
return False
|
|
651
|
+
print("已恢复到开工前状态。")
|
|
652
|
+
return True
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def cmd_start(args) -> None:
|
|
656
|
+
# 定位项目根
|
|
657
|
+
project_root = resolve_project_root()
|
|
658
|
+
# 找不到 .workflow_loop/ → 项目未安装(异常保护,不是正常业务分支)
|
|
659
|
+
if project_root is None:
|
|
660
|
+
print("错误:找不到 .workflow_loop/ 目录。请先在项目根执行官方安装脚本。")
|
|
661
|
+
sys.exit(1)
|
|
662
|
+
|
|
663
|
+
# 校验完整骨架和安装版本标记;失败时立即报错,不读取或创建 state.json
|
|
664
|
+
if not project_mod.is_installed(project_root):
|
|
665
|
+
print("错误:项目安装骨架不完整或版本异常。请先在项目根执行官方安装脚本。")
|
|
666
|
+
sys.exit(1)
|
|
667
|
+
|
|
668
|
+
# 未完成的清场开工事务:start 负责先恢复,恢复失败则停止
|
|
669
|
+
if not handle_pending_start_transaction(project_root):
|
|
670
|
+
sys.exit(1)
|
|
671
|
+
|
|
672
|
+
# 不带 --intent → 只读状态检查:只读取工作状态并指路,不写任何文件
|
|
673
|
+
if args.intent is None:
|
|
674
|
+
existing = state_mod.load_state(project_root)
|
|
675
|
+
# 有进行中 Run → 说明须继续原流程,禁止提示开新 Run
|
|
676
|
+
if existing is not None and existing.run_status == "active":
|
|
677
|
+
print(f"有进行中的工作轮次(编号: {existing.workflow_id})")
|
|
678
|
+
print(f"当前环节: {stage_label(existing.current_stage)}")
|
|
679
|
+
intent_label = INTENT_LABELS.get(existing.intent, existing.intent)
|
|
680
|
+
print(f"工作意图: {existing.intent}({intent_label.split(':')[0]})")
|
|
681
|
+
print_next_step(
|
|
682
|
+
"由 AI 执行 `workflow status` 查看详情并继续当前环节;"
|
|
683
|
+
"用户想结束本轮时,由 AI 执行 `workflow done`(正式收工)或 "
|
|
684
|
+
"`workflow abort`(整轮作废并恢复项目内容)"
|
|
685
|
+
)
|
|
686
|
+
return
|
|
687
|
+
# 无进行中 Run → 列出三种意图及一句话说明
|
|
688
|
+
print("当前没有进行中的工作轮次。可选工作意图:")
|
|
689
|
+
for intent in INTENT_CHOICES:
|
|
690
|
+
print(f" {intent}({INTENT_LABELS.get(intent, intent)})")
|
|
691
|
+
print_next_step(
|
|
692
|
+
"AI 根据用户需求确认工作意图后,执行 "
|
|
693
|
+
"`workflow start --intent from_scratch|product_change|bugfix` 开始新一轮工作;"
|
|
694
|
+
"这一步只初始化流程状态,不修改产品代码"
|
|
695
|
+
)
|
|
696
|
+
return
|
|
697
|
+
|
|
698
|
+
# 带 --intent → 初始化 Run
|
|
699
|
+
intent = args.intent
|
|
700
|
+
|
|
701
|
+
# Active Run Guard:有进行中 Run → 禁止再 start
|
|
702
|
+
if state_mod.is_active_run(project_root):
|
|
703
|
+
print("错误:有进行中 Run。请先 `workflow done` 或 `workflow abort` 结束当前 Run。")
|
|
704
|
+
sys.exit(1)
|
|
705
|
+
|
|
706
|
+
# from_scratch 的 Clean Confirm 两段式:先探测,有产物且未确认时只打印清单
|
|
707
|
+
clean_targets: list[str] = []
|
|
708
|
+
if intent == "from_scratch":
|
|
709
|
+
clean_targets = detect_clean_artifacts(project_root)
|
|
710
|
+
if clean_targets and not args.confirm_clean:
|
|
711
|
+
print("检测到以下过程产物包含内容;确认后会删除命中的整个目录及其中全部内容:")
|
|
712
|
+
for item in clean_targets:
|
|
713
|
+
suffix = "/" if not item.endswith(".md") else ""
|
|
714
|
+
print(f" {item}{suffix}")
|
|
715
|
+
print("从零做表示真正重新做:这些目录中即使有非工作流文件也会一起删除。")
|
|
716
|
+
print("本次尚未删除任何内容,也没有开始新轮次。")
|
|
717
|
+
print_next_step(
|
|
718
|
+
"AI 向用户说明以上将删清单;用户同意清场后,AI 执行 "
|
|
719
|
+
"`workflow start --intent from_scratch --confirm-clean` 完成清场并开工"
|
|
720
|
+
)
|
|
721
|
+
return
|
|
722
|
+
|
|
723
|
+
# 生成 workflow_id:YYYY-MM-DD-HHmm-<intent>
|
|
724
|
+
now = state_mod.now_iso()
|
|
725
|
+
date_part = now[:10]
|
|
726
|
+
# 去掉冒号避免文件名问题
|
|
727
|
+
time_part = now[11:16].replace(":", "")
|
|
728
|
+
workflow_id = f"{date_part}-{time_part}-{intent}"
|
|
729
|
+
|
|
730
|
+
# 新轮次第一次持久写入前:保存受管正式文档、项目字段和开工前 state.json。
|
|
731
|
+
# 副本保存在本轮回退目录中,同时作为整轮作废(abort)的开工基线。
|
|
732
|
+
try:
|
|
733
|
+
project_fields = project_mod.snapshot_managed_fields(project_root)
|
|
734
|
+
except ValueError as exc:
|
|
735
|
+
print(f"错误:无法保存开工前项目配置:{exc}")
|
|
736
|
+
sys.exit(1)
|
|
737
|
+
state_path = os.path.join(project_root, WORKFLOW_LOOP_DIRNAME, "state.json")
|
|
738
|
+
previous_state_raw = None
|
|
739
|
+
if os.path.isfile(state_path):
|
|
740
|
+
with open(state_path, "r", encoding="utf-8") as stream:
|
|
741
|
+
previous_state_raw = stream.read()
|
|
742
|
+
manifest = rollback_mod.prepare_start_baseline(
|
|
743
|
+
project_root,
|
|
744
|
+
workflow_id,
|
|
745
|
+
project_fields,
|
|
746
|
+
previous_state_raw,
|
|
747
|
+
clean_paths=clean_targets if intent == "from_scratch" else None,
|
|
748
|
+
)
|
|
749
|
+
journal_mod.append_entry(
|
|
750
|
+
project_root,
|
|
751
|
+
"开工回退基线",
|
|
752
|
+
"workflow.py",
|
|
753
|
+
workflow_id=workflow_id,
|
|
754
|
+
saved_documents=len(manifest.get("entries", {})),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# 清场属于破坏性动作:删除前写开工事务记录,成功提交后删除
|
|
758
|
+
do_clean = intent == "from_scratch" and bool(clean_targets)
|
|
759
|
+
if do_clean:
|
|
760
|
+
rollback_mod.write_start_transaction(project_root, workflow_id, clean_targets)
|
|
761
|
+
|
|
762
|
+
cleaned: list[str] = []
|
|
763
|
+
try:
|
|
764
|
+
if do_clean:
|
|
765
|
+
cleaned = clean_artifacts(project_root)
|
|
766
|
+
# 无论是否发现并删除旧产物,从零做都把 project_design_initialized 置为 false
|
|
767
|
+
if intent == "from_scratch":
|
|
768
|
+
project_mod.set_project_design_initialized(project_root, False)
|
|
769
|
+
|
|
770
|
+
# 调 PathComposer 生成 stage 列表
|
|
771
|
+
stages = build_stage_path(intent, project_root)
|
|
772
|
+
# 提取 stage 名列表存入 state.stage_path
|
|
773
|
+
stage_path = [s.name() for s in stages]
|
|
774
|
+
|
|
775
|
+
# 初始化每个 stage 的状态
|
|
776
|
+
stages_state = {}
|
|
777
|
+
for stage in stages:
|
|
778
|
+
stages_state[stage.name()] = state_mod.StageState(
|
|
779
|
+
status="pending",
|
|
780
|
+
artifact_paths=stage.artifact_paths(),
|
|
781
|
+
artifact_produced_at=None,
|
|
782
|
+
gate=state_mod.GateState(),
|
|
783
|
+
)
|
|
784
|
+
# 第一个 stage 标记为 in_progress
|
|
785
|
+
first_stage_name = stages[0].name()
|
|
786
|
+
stages_state[first_stage_name].status = "in_progress"
|
|
787
|
+
|
|
788
|
+
# 组装 WorkflowState(全集 schema)
|
|
789
|
+
wf_state = state_mod.WorkflowState(
|
|
790
|
+
workflow_id=workflow_id,
|
|
791
|
+
intent=intent,
|
|
792
|
+
run_status="active",
|
|
793
|
+
current_stage=first_stage_name,
|
|
794
|
+
started_at=now,
|
|
795
|
+
stage_path=stage_path,
|
|
796
|
+
stages=stages_state,
|
|
797
|
+
clean_confirmed=args.confirm_clean if intent == "from_scratch" else False,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# 保存 state.json
|
|
801
|
+
state_mod.save_state(project_root, wf_state)
|
|
802
|
+
|
|
803
|
+
# 写 journal:工作流启动 / 路径生成 / 清场确认
|
|
804
|
+
journal_mod.append_entry(project_root, "工作流启动", "ai",
|
|
805
|
+
workflow_id=workflow_id, intent=intent)
|
|
806
|
+
journal_mod.append_entry(project_root, "路径生成", "workflow.py",
|
|
807
|
+
intent=intent, stage_path=stage_path)
|
|
808
|
+
if do_clean:
|
|
809
|
+
journal_mod.append_entry(project_root, "清场确认", "workflow.py",
|
|
810
|
+
workflow_id=workflow_id, cleaned_paths=cleaned)
|
|
811
|
+
# 清场、项目字段、状态和启动日志全部成功后,事务才标记已提交并删除
|
|
812
|
+
rollback_mod.mark_start_transaction_committed(project_root, workflow_id)
|
|
813
|
+
rollback_mod.clear_start_transaction(project_root)
|
|
814
|
+
except Exception as exc: # noqa: BLE001 - 开工事务必须兜住任何失败并恢复
|
|
815
|
+
print(f"错误:开工过程失败({exc}),正在恢复开工前状态...")
|
|
816
|
+
failures = _restore_start_failure(project_root, workflow_id, manifest)
|
|
817
|
+
if failures:
|
|
818
|
+
print("恢复不完整,以下内容未恢复(事务记录和副本已保留,下次 start 会先恢复):")
|
|
819
|
+
for item in failures:
|
|
820
|
+
print(f" - {item}")
|
|
821
|
+
else:
|
|
822
|
+
print("已恢复到开工前状态;本次没有开始新轮次。")
|
|
823
|
+
sys.exit(1)
|
|
824
|
+
|
|
825
|
+
# 打印路径向开工摘要(不倾倒文档百科)
|
|
826
|
+
print(f"═══ 工作流启动 ═══")
|
|
827
|
+
print(f"workflow_id: {workflow_id}")
|
|
828
|
+
print(f"intent: {intent}({INTENT_LABELS.get(intent, intent).split(':')[0]})")
|
|
829
|
+
print(f"stage_path: {' → '.join(stage_path)}")
|
|
830
|
+
print(f"当前 stage: {stage_label(first_stage_name)}")
|
|
831
|
+
if cleaned:
|
|
832
|
+
print(f"已清场: {cleaned}")
|
|
833
|
+
# product_change/bugfix 显示 project_design_initialized 状态
|
|
834
|
+
if intent == "product_change" or intent == "bugfix":
|
|
835
|
+
pdi = project_mod.is_project_design_initialized(project_root)
|
|
836
|
+
print(f"project_design_initialized: {pdi}")
|
|
837
|
+
|
|
838
|
+
# 下一步:discuss
|
|
839
|
+
print_next_step(
|
|
840
|
+
"由 AI 执行 `workflow discuss`:它列出当前环节必须读取的材料文件绝对路径和用途,"
|
|
841
|
+
"AI 再用文件读取工具逐份读取;用户不需要手动执行命令"
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
# discuss 命令:给当前 AI 指出本 stage 必须读取的工作材料。
|
|
846
|
+
# 不重复打印文件正文:只输出经过检查的必读文件绝对路径、用途、读取顺序和产出路径。
|
|
847
|
+
# AI 收到清单后必须用文件读取工具逐份读取全文(Material File Reading)。
|
|
848
|
+
def cmd_discuss(args) -> None:
|
|
849
|
+
# 定位项目根
|
|
850
|
+
project_root = resolve_project_root()
|
|
851
|
+
|
|
852
|
+
if project_root is None:
|
|
853
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
854
|
+
sys.exit(1)
|
|
855
|
+
|
|
856
|
+
refuse_if_pending_start_transaction(project_root)
|
|
857
|
+
|
|
858
|
+
# 读 state
|
|
859
|
+
wf_state = state_mod.load_state(project_root)
|
|
860
|
+
# state 不存在 → 还没 start
|
|
861
|
+
if wf_state is None:
|
|
862
|
+
print("错误:还没启动工作流。调 `workflow start --intent <意图>` 开始。")
|
|
863
|
+
sys.exit(1)
|
|
864
|
+
# Run 已结束 → 不能 discuss
|
|
865
|
+
if wf_state.run_status != "active":
|
|
866
|
+
print(f"错误:Run 已 {wf_state.run_status},无法 discuss。")
|
|
867
|
+
sys.exit(1)
|
|
868
|
+
if ensure_stage_path_current(project_root, wf_state):
|
|
869
|
+
state_mod.save_state(project_root, wf_state)
|
|
870
|
+
if restore_recovery_context_from_journal(project_root, wf_state):
|
|
871
|
+
state_mod.save_state(project_root, wf_state)
|
|
872
|
+
clear_completed_material_recovery(project_root, wf_state)
|
|
873
|
+
if ensure_impl_recovery_baseline(project_root, wf_state):
|
|
874
|
+
state_mod.save_state(project_root, wf_state)
|
|
875
|
+
# 工作流已完成
|
|
876
|
+
if wf_state.current_stage == "completed":
|
|
877
|
+
print("错误:工作流已完成。")
|
|
878
|
+
sys.exit(1)
|
|
879
|
+
|
|
880
|
+
# 兼容旧状态:已经进入 spike 但没有入场基线时,在加载材料前标记无法还原
|
|
881
|
+
if wf_state.current_stage == "spike" and ensure_spike_baseline(project_root, wf_state):
|
|
882
|
+
state_mod.save_state(project_root, wf_state)
|
|
883
|
+
|
|
884
|
+
# 从 PathComposer 重建 stage 实例列表
|
|
885
|
+
stage_instances = build_stage_path(wf_state.intent, project_root)
|
|
886
|
+
# 找当前 stage 的策略
|
|
887
|
+
stage = get_stage_strategy(wf_state.current_stage, wf_state, stage_instances)
|
|
888
|
+
if stage is None:
|
|
889
|
+
print(f"错误:找不到 stage '{wf_state.current_stage}' 的策略实现")
|
|
890
|
+
sys.exit(1)
|
|
891
|
+
|
|
892
|
+
# 组装并校验材料清单:任一文件缺失、不是普通文件或不可读时命令失败,不登记清单
|
|
893
|
+
role_doc = role_doc_mod.get_role_doc(stage.name())
|
|
894
|
+
try:
|
|
895
|
+
checklist = stage_materials_mod.build_checklist(
|
|
896
|
+
project_root,
|
|
897
|
+
stage.name(),
|
|
898
|
+
role_doc,
|
|
899
|
+
stage.instruction(),
|
|
900
|
+
stage.materials(),
|
|
901
|
+
)
|
|
902
|
+
except MaterialError as exc:
|
|
903
|
+
print(f"═══ {stage_label(stage.name())} 材料清单检查失败 ═══")
|
|
904
|
+
print(f"详情: {exc}")
|
|
905
|
+
print("本次材料清单未登记。")
|
|
906
|
+
print_next_step("先恢复缺失或损坏的材料文件,再重新执行 `workflow discuss`")
|
|
907
|
+
sys.exit(1)
|
|
908
|
+
|
|
909
|
+
material_hash = checklist.fingerprint
|
|
910
|
+
stage_state = wf_state.stages[stage.name()]
|
|
911
|
+
if (
|
|
912
|
+
stage_state.discussion_material_hash is not None
|
|
913
|
+
and stage_state.discussion_material_hash != material_hash
|
|
914
|
+
):
|
|
915
|
+
# 材料内容变化:自动清除该阶段讨论完成和后续门禁状态,要求重读并重过第一道门。
|
|
916
|
+
# 只重复列出相同内容时不走这里、不回滚状态。
|
|
917
|
+
verification_mod.clear_stage_gates(stage_state)
|
|
918
|
+
stage_state.status = "in_progress"
|
|
919
|
+
stage_index = wf_state.stage_path.index(stage.name())
|
|
920
|
+
verification_mod.set_recovery_context(
|
|
921
|
+
wf_state,
|
|
922
|
+
stage.name(),
|
|
923
|
+
wf_state.stage_path[stage_index:],
|
|
924
|
+
"当前阶段的流程模板或规范已经更新,旧讨论结论必须重新确认",
|
|
925
|
+
)
|
|
926
|
+
journal_mod.append_entry(
|
|
927
|
+
project_root,
|
|
928
|
+
"阶段材料变化导致讨论失效",
|
|
929
|
+
"workflow.py",
|
|
930
|
+
workflow_id=wf_state.workflow_id,
|
|
931
|
+
stage=stage.name(),
|
|
932
|
+
previous_material_hash=stage_state.discussion_material_hash,
|
|
933
|
+
current_material_hash=material_hash,
|
|
934
|
+
recovery_created_at=wf_state.recovery.created_at,
|
|
935
|
+
)
|
|
936
|
+
stage_state.discussion_material_hash = material_hash
|
|
937
|
+
automated_acceptance_records = []
|
|
938
|
+
if stage.name() == "topic_acceptance":
|
|
939
|
+
try:
|
|
940
|
+
automated_acceptance_records = acceptance_records_mod.ensure_automated_records(
|
|
941
|
+
project_root,
|
|
942
|
+
wf_state,
|
|
943
|
+
)
|
|
944
|
+
except ValueError:
|
|
945
|
+
automated_acceptance_records = []
|
|
946
|
+
for record in automated_acceptance_records:
|
|
947
|
+
journal_mod.append_entry(
|
|
948
|
+
project_root,
|
|
949
|
+
"自动化验收记录",
|
|
950
|
+
"workflow.py",
|
|
951
|
+
workflow_id=wf_state.workflow_id,
|
|
952
|
+
topic=record.topic,
|
|
953
|
+
criterion_id=record.criterion_id,
|
|
954
|
+
result=record.result,
|
|
955
|
+
record_id=record.record_id,
|
|
956
|
+
test_ids=record.test_ids,
|
|
957
|
+
)
|
|
958
|
+
if ensure_impl_recovery_baseline(project_root, wf_state):
|
|
959
|
+
state_mod.save_state(project_root, wf_state)
|
|
960
|
+
state_mod.save_state(project_root, wf_state)
|
|
961
|
+
|
|
962
|
+
# 打印材料清单(不倾倒正文)
|
|
963
|
+
print(f"═══ {stage_label(stage.name())} 材料清单 ═══")
|
|
964
|
+
if verification_mod.recovery_summary(wf_state):
|
|
965
|
+
print("\n【为什么重新进入当前阶段】")
|
|
966
|
+
print_recovery_details(wf_state)
|
|
967
|
+
print("\n【角色】")
|
|
968
|
+
if checklist.role_title or checklist.role_description:
|
|
969
|
+
print(f"{checklist.role_title}:{checklist.role_description}")
|
|
970
|
+
else:
|
|
971
|
+
print("(本阶段没有角色定义)")
|
|
972
|
+
print("\n【当前阶段任务】")
|
|
973
|
+
print(checklist.task_text)
|
|
974
|
+
print("\n【必读文件】按下列顺序用文件读取工具逐份读取全文;终端不再打印正文:")
|
|
975
|
+
for material in checklist.materials:
|
|
976
|
+
print(f" {material.order}. {material.absolute_path}")
|
|
977
|
+
print(f" 用途:{material.purpose}")
|
|
978
|
+
if checklist.placeholders:
|
|
979
|
+
print("\n【本阶段没有的材料】")
|
|
980
|
+
for placeholder in checklist.placeholders:
|
|
981
|
+
print(f" - {placeholder.purpose.split(':')[0]}:{placeholder.note}")
|
|
982
|
+
print("\n【约定产出路径】")
|
|
983
|
+
for artifact_path in stage.artifact_paths():
|
|
984
|
+
print(f" {artifact_path}")
|
|
985
|
+
if stage.name() == "topic_acceptance":
|
|
986
|
+
print("\n【当前主题验收进度】")
|
|
987
|
+
for line in acceptance_records_mod.acceptance_progress(project_root, wf_state):
|
|
988
|
+
print(f"- {line}")
|
|
989
|
+
|
|
990
|
+
# 写 journal:材料清单登记(记录本次清单的组成与内容指纹)
|
|
991
|
+
journal_mod.append_entry(project_root, "材料清单登记", "workflow.py",
|
|
992
|
+
workflow_id=wf_state.workflow_id,
|
|
993
|
+
stage=stage.name(), prompt_doc=stage.prompt_doc_path(),
|
|
994
|
+
standard_doc=stage.standard_doc_path(),
|
|
995
|
+
additional_standard_docs=stage.additional_standard_doc_paths(),
|
|
996
|
+
global_writing_standard=GLOBAL_WRITING_STANDARD_PATH,
|
|
997
|
+
material_paths=[m.relative_path for m in checklist.materials],
|
|
998
|
+
material_hash=material_hash)
|
|
999
|
+
# 写 journal:角色文档加载
|
|
1000
|
+
journal_mod.append_entry(project_root, "角色文档加载", "workflow.py",
|
|
1001
|
+
stage=stage.name())
|
|
1002
|
+
|
|
1003
|
+
print_next_step(
|
|
1004
|
+
"AI 必须先用文件读取工具逐份读取上面清单里的全部文件;读取失败时停下并报告,"
|
|
1005
|
+
"不能假装已经读取。随后按阶段工作规范调查并与用户讨论。"
|
|
1006
|
+
f"用户明确表示讨论完毕后,AI 执行 `workflow gate {stage.name()} --discuss-done`"
|
|
1007
|
+
"(只记录\"当前环节已经聊清楚,可以开始写产物\",不代表产物已完成或已获用户认可)"
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _has_loaded_stage_materials(
|
|
1012
|
+
project_root: str,
|
|
1013
|
+
workflow_state: state_mod.WorkflowState,
|
|
1014
|
+
stage,
|
|
1015
|
+
) -> bool:
|
|
1016
|
+
"""确认当前工作流已经通过 workflow discuss 加载当前阶段全部材料。"""
|
|
1017
|
+
|
|
1018
|
+
def belongs_to_current_workflow(entry: dict) -> bool:
|
|
1019
|
+
# 新日志直接使用 workflow_id 区分不同工作流。
|
|
1020
|
+
entry_workflow_id = entry.get("workflow_id")
|
|
1021
|
+
if entry_workflow_id is not None:
|
|
1022
|
+
return entry_workflow_id == workflow_state.workflow_id
|
|
1023
|
+
|
|
1024
|
+
# 兼容新增 workflow_id 之前写入的旧日志:
|
|
1025
|
+
# 没有 workflow_id 时,只接受当前工作流启动之后的记录,避免误用更早 Run 的记录。
|
|
1026
|
+
entry_ts = entry.get("ts")
|
|
1027
|
+
if not entry_ts or not workflow_state.started_at:
|
|
1028
|
+
return False
|
|
1029
|
+
try:
|
|
1030
|
+
return datetime.fromisoformat(entry_ts) >= datetime.fromisoformat(workflow_state.started_at)
|
|
1031
|
+
except ValueError:
|
|
1032
|
+
return False
|
|
1033
|
+
|
|
1034
|
+
required_standard_docs = set(stage.additional_standard_doc_paths())
|
|
1035
|
+
try:
|
|
1036
|
+
current_checklist = stage_materials_mod.build_checklist(
|
|
1037
|
+
project_root,
|
|
1038
|
+
stage.name(),
|
|
1039
|
+
role_doc_mod.get_role_doc(stage.name()),
|
|
1040
|
+
stage.instruction(),
|
|
1041
|
+
stage.materials(),
|
|
1042
|
+
)
|
|
1043
|
+
except MaterialError:
|
|
1044
|
+
# 材料缺失或不可读:清单记录不能视为有效
|
|
1045
|
+
return False
|
|
1046
|
+
current_material_hash = current_checklist.fingerprint
|
|
1047
|
+
current_material_paths = [
|
|
1048
|
+
material.relative_path for material in current_checklist.materials
|
|
1049
|
+
]
|
|
1050
|
+
saved_material_hash = workflow_state.stages.get(
|
|
1051
|
+
stage.name(),
|
|
1052
|
+
state_mod.StageState(),
|
|
1053
|
+
).discussion_material_hash
|
|
1054
|
+
if saved_material_hash is not None and saved_material_hash != current_material_hash:
|
|
1055
|
+
return False
|
|
1056
|
+
for entry in reversed(journal_mod.read_all(project_root)):
|
|
1057
|
+
if entry.get("action") != "材料清单登记":
|
|
1058
|
+
continue
|
|
1059
|
+
if not belongs_to_current_workflow(entry):
|
|
1060
|
+
continue
|
|
1061
|
+
if entry.get("stage") != stage.name():
|
|
1062
|
+
continue
|
|
1063
|
+
if entry.get("prompt_doc") != stage.prompt_doc_path():
|
|
1064
|
+
continue
|
|
1065
|
+
if entry.get("standard_doc") != stage.standard_doc_path():
|
|
1066
|
+
continue
|
|
1067
|
+
loaded_standard_docs = set(entry.get("additional_standard_docs", []))
|
|
1068
|
+
if (
|
|
1069
|
+
required_standard_docs.issubset(loaded_standard_docs)
|
|
1070
|
+
and entry.get("material_paths") == current_material_paths
|
|
1071
|
+
and entry.get("material_hash") == current_material_hash
|
|
1072
|
+
):
|
|
1073
|
+
return True
|
|
1074
|
+
return False
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _register_stage_artifact_keys(
|
|
1078
|
+
project_root: str,
|
|
1079
|
+
wf_state: state_mod.WorkflowState,
|
|
1080
|
+
stage_name: str,
|
|
1081
|
+
) -> dict[str, str]:
|
|
1082
|
+
"""在正式产物通过程序检查后登记稳定文件标识,并核对真实路径。"""
|
|
1083
|
+
project = project_mod.load_project(project_root)
|
|
1084
|
+
if project is None:
|
|
1085
|
+
raise ValueError("项目尚未安装,不能登记正式文件标识")
|
|
1086
|
+
|
|
1087
|
+
category = ""
|
|
1088
|
+
paths_by_name: dict[str, str] = {}
|
|
1089
|
+
path_builder = None
|
|
1090
|
+
if stage_name == "spec":
|
|
1091
|
+
category = "feature"
|
|
1092
|
+
path_builder = artifact_paths_mod.feature_doc
|
|
1093
|
+
for relative_path in verification_mod.get_linked_product_design_paths(project_root):
|
|
1094
|
+
if relative_path == artifact_paths_mod.PRODUCT_OVERVIEW_DOC:
|
|
1095
|
+
continue
|
|
1096
|
+
with open(os.path.join(project_root, relative_path), "r", encoding="utf-8") as stream:
|
|
1097
|
+
title = stream.readline().strip()
|
|
1098
|
+
display_name = title.removeprefix("# 【功能】").strip()
|
|
1099
|
+
if display_name:
|
|
1100
|
+
paths_by_name[display_name] = relative_path.replace(os.sep, "/")
|
|
1101
|
+
elif stage_name == "spike" and not wf_state.spike_skipped:
|
|
1102
|
+
category = "spike"
|
|
1103
|
+
path_builder = artifact_paths_mod.spike_doc
|
|
1104
|
+
index_path = os.path.join(project_root, artifact_paths_mod.SPIKE_INDEX_DOC)
|
|
1105
|
+
_workflow_id, items, errors = spike_validation_mod.parse_spike_index(index_path)
|
|
1106
|
+
if errors:
|
|
1107
|
+
raise ValueError("穿刺清单仍有错误,不能登记文件标识: " + ";".join(errors))
|
|
1108
|
+
for item in items:
|
|
1109
|
+
link = item.fields.get("结论文档", "")
|
|
1110
|
+
match = re.search(r"\[[^\]]+\]\(([^)#]+)", link)
|
|
1111
|
+
if match is None:
|
|
1112
|
+
raise ValueError(f"穿刺项 {item.item_id} 的结论文档链接无效")
|
|
1113
|
+
target = match.group(1).strip()
|
|
1114
|
+
if target.startswith("./"):
|
|
1115
|
+
target = target[2:]
|
|
1116
|
+
if not target.startswith("spec/"):
|
|
1117
|
+
target = f"spec/{target}"
|
|
1118
|
+
paths_by_name[item.name] = target.replace("\\", "/")
|
|
1119
|
+
elif stage_name == "reproduce":
|
|
1120
|
+
category = "bug"
|
|
1121
|
+
path_builder = artifact_paths_mod.bug_doc
|
|
1122
|
+
bug_dir = os.path.join(project_root, "bug")
|
|
1123
|
+
if os.path.isdir(bug_dir):
|
|
1124
|
+
for filename in sorted(os.listdir(bug_dir)):
|
|
1125
|
+
if not filename.startswith("缺陷_") or not filename.endswith(".md"):
|
|
1126
|
+
continue
|
|
1127
|
+
relative_path = f"bug/{filename}"
|
|
1128
|
+
with open(os.path.join(project_root, relative_path), "r", encoding="utf-8") as stream:
|
|
1129
|
+
content = stream.read()
|
|
1130
|
+
if f"- 工作流编号:{wf_state.workflow_id}" not in content:
|
|
1131
|
+
continue
|
|
1132
|
+
first_line = content.splitlines()[0].strip() if content.splitlines() else ""
|
|
1133
|
+
display_name = first_line.removeprefix("# 【缺陷】").strip()
|
|
1134
|
+
if display_name:
|
|
1135
|
+
paths_by_name[display_name] = relative_path
|
|
1136
|
+
else:
|
|
1137
|
+
return {}
|
|
1138
|
+
|
|
1139
|
+
if not paths_by_name or path_builder is None:
|
|
1140
|
+
return {}
|
|
1141
|
+
added = artifact_paths_mod.register_file_keys(
|
|
1142
|
+
project,
|
|
1143
|
+
category,
|
|
1144
|
+
list(paths_by_name),
|
|
1145
|
+
)
|
|
1146
|
+
mapping = project.artifact_file_keys.get(category, {})
|
|
1147
|
+
mismatches = []
|
|
1148
|
+
for display_name, actual_path in paths_by_name.items():
|
|
1149
|
+
expected_path = path_builder(mapping[display_name])
|
|
1150
|
+
if actual_path != expected_path:
|
|
1151
|
+
mismatches.append(f"{display_name!r}: 当前 {actual_path},应为 {expected_path}")
|
|
1152
|
+
if mismatches:
|
|
1153
|
+
raise ValueError("正式文件名与稳定文件标识不一致:" + ";".join(mismatches))
|
|
1154
|
+
if added:
|
|
1155
|
+
project_mod.save_project(project_root, project)
|
|
1156
|
+
return added
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def apply_stage_completion_updates(
|
|
1160
|
+
project_root: str,
|
|
1161
|
+
wf_state: state_mod.WorkflowState,
|
|
1162
|
+
stage_name: str,
|
|
1163
|
+
) -> list[str]:
|
|
1164
|
+
"""执行阶段确认后必须落盘的追踪表和缺陷状态更新。"""
|
|
1165
|
+
topics = topic_mod.current_workflow_topics(project_root)
|
|
1166
|
+
if stage_name in {
|
|
1167
|
+
"test_plan",
|
|
1168
|
+
"impl",
|
|
1169
|
+
"test_execution",
|
|
1170
|
+
"topic_acceptance",
|
|
1171
|
+
"regression_test",
|
|
1172
|
+
"overall_acceptance",
|
|
1173
|
+
"update_code_design",
|
|
1174
|
+
}:
|
|
1175
|
+
detail = traceability_mod.update_for_stage(
|
|
1176
|
+
project_root,
|
|
1177
|
+
wf_state.workflow_id,
|
|
1178
|
+
topics,
|
|
1179
|
+
stage_name,
|
|
1180
|
+
)
|
|
1181
|
+
journal_mod.append_entry(
|
|
1182
|
+
project_root,
|
|
1183
|
+
"需求交付追踪更新",
|
|
1184
|
+
"workflow.py",
|
|
1185
|
+
stage=stage_name,
|
|
1186
|
+
details=detail,
|
|
1187
|
+
)
|
|
1188
|
+
updates = [detail]
|
|
1189
|
+
else:
|
|
1190
|
+
updates = []
|
|
1191
|
+
|
|
1192
|
+
if wf_state.intent != "bugfix":
|
|
1193
|
+
return updates
|
|
1194
|
+
|
|
1195
|
+
if stage_name == "topic_acceptance":
|
|
1196
|
+
detail = bug_record_mod.record_topic_acceptance_pass(
|
|
1197
|
+
project_root,
|
|
1198
|
+
wf_state.workflow_id,
|
|
1199
|
+
topics,
|
|
1200
|
+
)
|
|
1201
|
+
elif stage_name == "regression_test":
|
|
1202
|
+
detail = bug_record_mod.record_regression_pass(
|
|
1203
|
+
project_root,
|
|
1204
|
+
wf_state.workflow_id,
|
|
1205
|
+
topics,
|
|
1206
|
+
)
|
|
1207
|
+
elif stage_name == "overall_acceptance":
|
|
1208
|
+
detail = bug_record_mod.record_overall_acceptance_pass(
|
|
1209
|
+
project_root,
|
|
1210
|
+
wf_state.workflow_id,
|
|
1211
|
+
topics,
|
|
1212
|
+
)
|
|
1213
|
+
else:
|
|
1214
|
+
return updates
|
|
1215
|
+
|
|
1216
|
+
journal_mod.append_entry(
|
|
1217
|
+
project_root,
|
|
1218
|
+
"缺陷状态更新",
|
|
1219
|
+
"workflow.py",
|
|
1220
|
+
stage=stage_name,
|
|
1221
|
+
details=detail,
|
|
1222
|
+
)
|
|
1223
|
+
updates.append(detail)
|
|
1224
|
+
return updates
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def validate_stage_output(
|
|
1228
|
+
project_root: str,
|
|
1229
|
+
wf_state: state_mod.WorkflowState,
|
|
1230
|
+
stage_name: str,
|
|
1231
|
+
stage: StageStrategy,
|
|
1232
|
+
*,
|
|
1233
|
+
execute_regression: bool = True,
|
|
1234
|
+
) -> tuple[bool, str]:
|
|
1235
|
+
"""执行阶段产物校验;最终回归可由调用方控制是否实际执行。"""
|
|
1236
|
+
|
|
1237
|
+
if stage_name == "regression_test" and execute_regression:
|
|
1238
|
+
passed, details = test_runner_mod.run_final_regression(project_root, wf_state)
|
|
1239
|
+
journal_mod.append_entry(
|
|
1240
|
+
project_root,
|
|
1241
|
+
"最终全量回归",
|
|
1242
|
+
"workflow.py",
|
|
1243
|
+
stage=stage_name,
|
|
1244
|
+
passed=passed,
|
|
1245
|
+
**test_runner_mod.regression_journal_fields(wf_state),
|
|
1246
|
+
)
|
|
1247
|
+
state_mod.save_state(project_root, wf_state)
|
|
1248
|
+
if not passed:
|
|
1249
|
+
return False, details
|
|
1250
|
+
|
|
1251
|
+
passed, details = stage.code_validate(project_root)
|
|
1252
|
+
return passed, details
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def regression_failure_next_step() -> str:
|
|
1256
|
+
"""回归失败时,告诉用户先归因再退回,避免无条件重复执行同一命令。"""
|
|
1257
|
+
return (
|
|
1258
|
+
"最终全量回归未通过,先查看 state.json/journal.jsonl 的命令、退出码和输出摘要,"
|
|
1259
|
+
"判断失败属于产品代码、测试代码、测试计划还是临时环境;"
|
|
1260
|
+
"确定原因后调 `workflow return --to <阶段> --reason \"具体原因\"` 返回对应阶段,"
|
|
1261
|
+
"不要直接重复调当前 regression_test 门禁"
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _load_active_workflow_for_command(project_root: str) -> state_mod.WorkflowState:
|
|
1266
|
+
refuse_if_pending_start_transaction(project_root)
|
|
1267
|
+
workflow_state = state_mod.load_state(project_root)
|
|
1268
|
+
if workflow_state is None:
|
|
1269
|
+
print("错误:还没启动工作流")
|
|
1270
|
+
sys.exit(1)
|
|
1271
|
+
if workflow_state.run_status != "active":
|
|
1272
|
+
print(f"错误:Run 已 {workflow_state.run_status},不能执行当前命令")
|
|
1273
|
+
sys.exit(1)
|
|
1274
|
+
if restore_recovery_context_from_journal(project_root, workflow_state):
|
|
1275
|
+
state_mod.save_state(project_root, workflow_state)
|
|
1276
|
+
if ensure_impl_recovery_baseline(project_root, workflow_state):
|
|
1277
|
+
state_mod.save_state(project_root, workflow_state)
|
|
1278
|
+
return workflow_state
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _test_execution_inputs_are_current(
|
|
1282
|
+
project_root: str,
|
|
1283
|
+
wf_state: state_mod.WorkflowState,
|
|
1284
|
+
) -> bool:
|
|
1285
|
+
"""测试登记和执行前先确认实施结果、计划和测试代码没有失效。"""
|
|
1286
|
+
invalidations = verification_mod.check_invalidation(wf_state, project_root)
|
|
1287
|
+
if invalidations:
|
|
1288
|
+
ensure_impl_recovery_baseline(project_root, wf_state)
|
|
1289
|
+
state_mod.save_state(project_root, wf_state)
|
|
1290
|
+
for from_stage, to_stages in invalidations:
|
|
1291
|
+
journal_mod.append_entry(
|
|
1292
|
+
project_root,
|
|
1293
|
+
"验证失效",
|
|
1294
|
+
"workflow.py",
|
|
1295
|
+
workflow_id=wf_state.workflow_id,
|
|
1296
|
+
from_stage=from_stage,
|
|
1297
|
+
to_stage=to_stages,
|
|
1298
|
+
reason=wf_state.recovery.reason or "测试登记或执行前发现上游内容变化",
|
|
1299
|
+
recovery_created_at=wf_state.recovery.created_at,
|
|
1300
|
+
)
|
|
1301
|
+
print("═══ 测试执行前置内容已失效 ═══")
|
|
1302
|
+
for from_stage, to_stages in invalidations:
|
|
1303
|
+
print(f"{from_stage} 变化,已退回 {to_stages}")
|
|
1304
|
+
print_recovery_details(wf_state)
|
|
1305
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1306
|
+
return False
|
|
1307
|
+
if wf_state.verification.test_code_hash is None:
|
|
1308
|
+
print("错误:缺少 test_code 确认后的测试代码哈希")
|
|
1309
|
+
print_next_step("先完成 test_code 阶段并通过用户确认")
|
|
1310
|
+
return False
|
|
1311
|
+
return True
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def _test_execution_materials_are_loaded(
|
|
1315
|
+
project_root: str,
|
|
1316
|
+
wf_state: state_mod.WorkflowState,
|
|
1317
|
+
) -> bool:
|
|
1318
|
+
"""测试登记和执行只接受本次 workflow discuss 加载的当前材料。"""
|
|
1319
|
+
stage = get_stage_strategy(
|
|
1320
|
+
"test_execution",
|
|
1321
|
+
wf_state,
|
|
1322
|
+
build_stage_path(wf_state.intent, project_root),
|
|
1323
|
+
)
|
|
1324
|
+
if stage is None:
|
|
1325
|
+
return False
|
|
1326
|
+
stage_state = wf_state.stages.get("test_execution")
|
|
1327
|
+
try:
|
|
1328
|
+
current_hash = compute_stage_material_hash(project_root, stage)
|
|
1329
|
+
except MaterialError:
|
|
1330
|
+
return False
|
|
1331
|
+
return (
|
|
1332
|
+
stage_state is not None
|
|
1333
|
+
and stage_state.discussion_material_hash == current_hash
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def cmd_test_entry(args) -> None:
|
|
1338
|
+
"""在测试计划环节登记项目全量测试入口配置;只登记,不运行任何测试。
|
|
1339
|
+
|
|
1340
|
+
仅供 AI 执行:入口按操作系统保存为命令参数数组;需要管道、重定向或
|
|
1341
|
+
多条命令时必须放入项目统一入口脚本。新建或修改的入口脚本先保存
|
|
1342
|
+
修改前内容进本轮回退清单,整轮作废时可以准确恢复。
|
|
1343
|
+
"""
|
|
1344
|
+
project_root = resolve_project_root()
|
|
1345
|
+
if project_root is None:
|
|
1346
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
1347
|
+
sys.exit(1)
|
|
1348
|
+
wf_state = _load_active_workflow_for_command(project_root)
|
|
1349
|
+
if wf_state.current_stage != "test_plan":
|
|
1350
|
+
print(f"错误:当前环节是 {stage_label(wf_state.current_stage)},"
|
|
1351
|
+
"只能在 test_plan(测试计划)环节登记项目测试入口")
|
|
1352
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1353
|
+
return
|
|
1354
|
+
stage_instances = build_stage_path(wf_state.intent, project_root)
|
|
1355
|
+
stage = get_stage_strategy("test_plan", wf_state, stage_instances)
|
|
1356
|
+
if stage is None or not _has_loaded_stage_materials(project_root, wf_state, stage):
|
|
1357
|
+
print("错误:还没有通过 workflow discuss 加载测试计划阶段的当前材料")
|
|
1358
|
+
print_next_step("先执行 `workflow discuss`,读取测试计划模板和工作规范")
|
|
1359
|
+
return
|
|
1360
|
+
stage_state = wf_state.stages.get("test_plan")
|
|
1361
|
+
if stage_state is None or not stage_state.gate.discussion_complete:
|
|
1362
|
+
print("错误:测试计划讨论还没有完成(第一道门未通过),不能登记测试入口")
|
|
1363
|
+
print_next_step("和用户确认测试覆盖后,先执行 `workflow gate test_plan --discuss-done`")
|
|
1364
|
+
return
|
|
1365
|
+
|
|
1366
|
+
entry_config = {}
|
|
1367
|
+
for platform_key in ("default", "windows", "linux", "darwin"):
|
|
1368
|
+
argv = getattr(args, platform_key, None)
|
|
1369
|
+
if argv:
|
|
1370
|
+
entry_config[platform_key] = list(argv)
|
|
1371
|
+
if not entry_config:
|
|
1372
|
+
print("错误:至少提供一个平台的入口参数数组,例如 --darwin .venv/bin/python -m pytest -q")
|
|
1373
|
+
return
|
|
1374
|
+
|
|
1375
|
+
declared_scripts = sorted(
|
|
1376
|
+
{
|
|
1377
|
+
script.replace("\\", "/")
|
|
1378
|
+
for script in (args.script or [])
|
|
1379
|
+
}
|
|
1380
|
+
)
|
|
1381
|
+
referenced_scripts = test_entry_mod.referenced_project_scripts(entry_config)
|
|
1382
|
+
undeclared_scripts = sorted(set(referenced_scripts) - set(declared_scripts))
|
|
1383
|
+
if undeclared_scripts:
|
|
1384
|
+
print("═══ 测试入口登记失败 ═══")
|
|
1385
|
+
print(f"详情: 入口参数引用了尚未登记回退依据的项目脚本: {undeclared_scripts}")
|
|
1386
|
+
print_next_step(
|
|
1387
|
+
"先对每个脚本重复使用 `--script <项目内相对路径>` 执行本命令;"
|
|
1388
|
+
"脚本原本不存在时必须在本命令成功后再创建"
|
|
1389
|
+
)
|
|
1390
|
+
return
|
|
1391
|
+
|
|
1392
|
+
# 回退依据:project.json 原字段已由开工基线保存;声明的入口脚本先登记原内容。
|
|
1393
|
+
for script in declared_scripts:
|
|
1394
|
+
try:
|
|
1395
|
+
script_detail = rollback_mod.register_start_entry_script(
|
|
1396
|
+
project_root,
|
|
1397
|
+
wf_state.workflow_id,
|
|
1398
|
+
script,
|
|
1399
|
+
)
|
|
1400
|
+
except (ValueError, OSError) as exc:
|
|
1401
|
+
print("═══ 测试入口登记失败 ═══")
|
|
1402
|
+
print(f"详情: 无法保存入口脚本的修改前内容:{exc}")
|
|
1403
|
+
print_next_step("修正脚本路径后重新执行 `workflow test entry`")
|
|
1404
|
+
return
|
|
1405
|
+
print(f"入口脚本回退依据: {script}({script_detail})")
|
|
1406
|
+
|
|
1407
|
+
try:
|
|
1408
|
+
project_mod.register_test_entry(project_root, entry_config)
|
|
1409
|
+
except ValueError as exc:
|
|
1410
|
+
print("═══ 测试入口登记失败 ═══")
|
|
1411
|
+
print(f"详情: {exc}")
|
|
1412
|
+
print_next_step("修正入口参数数组后重新执行 `workflow test entry`;"
|
|
1413
|
+
"复杂命令请放入项目统一入口脚本")
|
|
1414
|
+
return
|
|
1415
|
+
|
|
1416
|
+
journal_mod.append_entry(
|
|
1417
|
+
project_root,
|
|
1418
|
+
"项目测试入口登记",
|
|
1419
|
+
"user",
|
|
1420
|
+
workflow_id=wf_state.workflow_id,
|
|
1421
|
+
test_entry=entry_config,
|
|
1422
|
+
declared_scripts=declared_scripts,
|
|
1423
|
+
)
|
|
1424
|
+
print("═══ 项目全量测试入口已登记 ═══")
|
|
1425
|
+
for platform_key, argv in entry_config.items():
|
|
1426
|
+
print(f" {platform_key}: {argv}")
|
|
1427
|
+
print("本命令只登记入口配置,不运行任何测试;全量测试只在最终回归阶段执行。")
|
|
1428
|
+
print_next_step(
|
|
1429
|
+
"确认测试计划文档完整后,由 AI 执行 `workflow gate test_plan` 做程序校验"
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def cmd_test_prepare(args) -> None:
|
|
1434
|
+
"""登记一个测试项的真实 argv 命令,不执行命令。"""
|
|
1435
|
+
project_root = resolve_project_root()
|
|
1436
|
+
if project_root is None:
|
|
1437
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
1438
|
+
sys.exit(1)
|
|
1439
|
+
wf_state = _load_active_workflow_for_command(project_root)
|
|
1440
|
+
if wf_state.current_stage != "test_execution":
|
|
1441
|
+
print(f"错误:当前 stage 是 {wf_state.current_stage},不能登记主题测试命令")
|
|
1442
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1443
|
+
return
|
|
1444
|
+
if not _test_execution_materials_are_loaded(project_root, wf_state):
|
|
1445
|
+
print("错误:还没有通过 workflow discuss 加载当前测试执行模板和规范")
|
|
1446
|
+
print_next_step("先调 `workflow discuss`,阅读当前测试执行模板和规范")
|
|
1447
|
+
return
|
|
1448
|
+
if not _test_execution_inputs_are_current(project_root, wf_state):
|
|
1449
|
+
return
|
|
1450
|
+
|
|
1451
|
+
command = list(args.command_argv or [])
|
|
1452
|
+
if command and command[0] == "--":
|
|
1453
|
+
command = command[1:]
|
|
1454
|
+
try:
|
|
1455
|
+
task = test_execution_mod.prepare_task(
|
|
1456
|
+
project_root,
|
|
1457
|
+
wf_state,
|
|
1458
|
+
args.topic,
|
|
1459
|
+
args.tc,
|
|
1460
|
+
command,
|
|
1461
|
+
args.timeout,
|
|
1462
|
+
cwd=args.cwd,
|
|
1463
|
+
)
|
|
1464
|
+
except ValueError as exc:
|
|
1465
|
+
print("═══ 测试命令登记失败 ═══")
|
|
1466
|
+
print(f"详情: {exc}")
|
|
1467
|
+
print_next_step("补齐测试计划、测试入口或安全命令后重新调 `workflow test prepare`")
|
|
1468
|
+
return
|
|
1469
|
+
|
|
1470
|
+
journal_mod.append_entry(
|
|
1471
|
+
project_root,
|
|
1472
|
+
"测试项任务登记",
|
|
1473
|
+
"user",
|
|
1474
|
+
workflow_id=wf_state.workflow_id,
|
|
1475
|
+
topic=args.topic,
|
|
1476
|
+
test_id=args.tc,
|
|
1477
|
+
test_entries=task.test_entries,
|
|
1478
|
+
command=task.command,
|
|
1479
|
+
cwd=task.cwd,
|
|
1480
|
+
dependencies=task.dependencies,
|
|
1481
|
+
timeout_seconds=task.timeout_seconds,
|
|
1482
|
+
)
|
|
1483
|
+
state_mod.save_state(project_root, wf_state)
|
|
1484
|
+
print("═══ 测试项任务已登记 ═══")
|
|
1485
|
+
print(f"主题: {args.topic}")
|
|
1486
|
+
print(f"测试项: {args.tc}")
|
|
1487
|
+
print(f"测试入口: {', '.join(task.test_entries)}")
|
|
1488
|
+
print(f"执行命令: {' '.join(task.command)}")
|
|
1489
|
+
print(f"工作目录: {task.cwd or '项目根'}")
|
|
1490
|
+
print(f"前置测试项: {', '.join(task.dependencies) if task.dependencies else '无'}")
|
|
1491
|
+
print(f"超时: {task.timeout_seconds} 秒")
|
|
1492
|
+
missing = test_execution_mod.missing_prepared_tasks(project_root, wf_state)
|
|
1493
|
+
if missing:
|
|
1494
|
+
print_next_step(f"继续登记剩余测试项: {missing}")
|
|
1495
|
+
else:
|
|
1496
|
+
print_next_step("确认所有测试项和命令后,调 `workflow gate test_execution --discuss-done`")
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def cmd_test_run(args) -> None:
|
|
1500
|
+
"""执行已经登记的主题测试任务。"""
|
|
1501
|
+
project_root = resolve_project_root()
|
|
1502
|
+
if project_root is None:
|
|
1503
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
1504
|
+
sys.exit(1)
|
|
1505
|
+
wf_state = _load_active_workflow_for_command(project_root)
|
|
1506
|
+
if wf_state.current_stage != "test_execution":
|
|
1507
|
+
print(f"错误:当前 stage 是 {wf_state.current_stage},不能执行主题测试")
|
|
1508
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1509
|
+
return
|
|
1510
|
+
if not _test_execution_materials_are_loaded(project_root, wf_state):
|
|
1511
|
+
print("错误:当前测试执行材料没有加载,或加载后内容已经变化")
|
|
1512
|
+
print_next_step("先调 `workflow discuss`,重新阅读当前测试执行模板和规范")
|
|
1513
|
+
return
|
|
1514
|
+
if not _test_execution_inputs_are_current(project_root, wf_state):
|
|
1515
|
+
return
|
|
1516
|
+
stage_state = wf_state.stages.get("test_execution")
|
|
1517
|
+
if stage_state is None or not stage_state.gate.discussion_complete:
|
|
1518
|
+
print("错误:测试执行任务还没有经过讨论确认")
|
|
1519
|
+
print_next_step("先登记全部测试项,再调 `workflow gate test_execution --discuss-done`")
|
|
1520
|
+
return
|
|
1521
|
+
try:
|
|
1522
|
+
attempts = test_execution_mod.run_prepared_tasks(
|
|
1523
|
+
project_root,
|
|
1524
|
+
wf_state,
|
|
1525
|
+
args.parallel,
|
|
1526
|
+
)
|
|
1527
|
+
except ValueError as exc:
|
|
1528
|
+
print("═══ 测试执行未开始 ═══")
|
|
1529
|
+
print(f"详情: {exc}")
|
|
1530
|
+
print_next_step("补齐测试任务登记后重新调 `workflow test run`")
|
|
1531
|
+
return
|
|
1532
|
+
|
|
1533
|
+
print("═══ 主题测试执行完成 ═══")
|
|
1534
|
+
print(test_execution_mod.summarize_attempts(attempts))
|
|
1535
|
+
for attempt in attempts:
|
|
1536
|
+
print(f"- {attempt.topic} / {attempt.test_id}: {attempt.status}")
|
|
1537
|
+
if attempt.status != "passed":
|
|
1538
|
+
if attempt.error:
|
|
1539
|
+
print(f" 原因: {attempt.error}")
|
|
1540
|
+
if attempt.output_tail:
|
|
1541
|
+
print(" 输出摘要:")
|
|
1542
|
+
print(attempt.output_tail)
|
|
1543
|
+
failed = [attempt for attempt in attempts if attempt.status != "passed"]
|
|
1544
|
+
if failed:
|
|
1545
|
+
print_next_step("先判断问题属于 impl、test_code、test_plan、acceptance_plan、spec 或临时环境,再调 `workflow return --to ...`")
|
|
1546
|
+
else:
|
|
1547
|
+
automated_topics = test_mapping_mod.automated_topics(project_root, wf_state.topics)
|
|
1548
|
+
if automated_topics:
|
|
1549
|
+
print_next_step(
|
|
1550
|
+
"根据当前成功记录补齐或复核各主题 "
|
|
1551
|
+
"`qa/<主题文件标识>_测试结果.md`,再调 `workflow gate test_execution`"
|
|
1552
|
+
)
|
|
1553
|
+
else:
|
|
1554
|
+
print_next_step("当前没有自动化测试结果文件需要生成,直接调 `workflow gate test_execution`")
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def cmd_acceptance_record(args) -> None:
|
|
1558
|
+
"""记录一条人工或混合验收条件的用户回答。"""
|
|
1559
|
+
project_root = resolve_project_root()
|
|
1560
|
+
if project_root is None:
|
|
1561
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
1562
|
+
sys.exit(1)
|
|
1563
|
+
wf_state = _load_active_workflow_for_command(project_root)
|
|
1564
|
+
if wf_state.current_stage != "topic_acceptance":
|
|
1565
|
+
print(f"错误:当前 stage 是 {wf_state.current_stage},不能记录主题验收回答")
|
|
1566
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1567
|
+
return
|
|
1568
|
+
try:
|
|
1569
|
+
created = acceptance_records_mod.ensure_automated_records(project_root, wf_state)
|
|
1570
|
+
record = acceptance_records_mod.record_user_result(
|
|
1571
|
+
project_root,
|
|
1572
|
+
wf_state,
|
|
1573
|
+
topic=args.topic,
|
|
1574
|
+
criterion_id=args.criterion,
|
|
1575
|
+
result=args.result,
|
|
1576
|
+
actual_result=args.actual_result,
|
|
1577
|
+
user_answer=args.answer,
|
|
1578
|
+
evidence=args.evidence or "",
|
|
1579
|
+
)
|
|
1580
|
+
except ValueError as exc:
|
|
1581
|
+
print("═══ 主题验收记录失败 ═══")
|
|
1582
|
+
print(f"详情: {exc}")
|
|
1583
|
+
print_next_step("先把当前问题问清楚;用户确认后再记录,不能直接生成主题验收结果")
|
|
1584
|
+
return
|
|
1585
|
+
|
|
1586
|
+
for auto_record in created:
|
|
1587
|
+
journal_mod.append_entry(
|
|
1588
|
+
project_root,
|
|
1589
|
+
"自动化验收记录",
|
|
1590
|
+
"workflow.py",
|
|
1591
|
+
workflow_id=wf_state.workflow_id,
|
|
1592
|
+
topic=auto_record.topic,
|
|
1593
|
+
criterion_id=auto_record.criterion_id,
|
|
1594
|
+
result=auto_record.result,
|
|
1595
|
+
record_id=auto_record.record_id,
|
|
1596
|
+
test_ids=auto_record.test_ids,
|
|
1597
|
+
)
|
|
1598
|
+
action = "人工验收记录" if record.result == "passed" else "主题验收问题记录"
|
|
1599
|
+
journal_mod.append_entry(
|
|
1600
|
+
project_root,
|
|
1601
|
+
action,
|
|
1602
|
+
"user",
|
|
1603
|
+
workflow_id=wf_state.workflow_id,
|
|
1604
|
+
topic=record.topic,
|
|
1605
|
+
criterion_id=record.criterion_id,
|
|
1606
|
+
result=record.result,
|
|
1607
|
+
actual_result=record.actual_result,
|
|
1608
|
+
user_answer=record.user_answer,
|
|
1609
|
+
evidence=record.evidence,
|
|
1610
|
+
confirmed_at=record.confirmed_at,
|
|
1611
|
+
record_id=record.record_id,
|
|
1612
|
+
)
|
|
1613
|
+
state_mod.save_state(project_root, wf_state)
|
|
1614
|
+
print("═══ 主题验收回答已记录 ═══" if record.result == "passed" else "═══ 主题验收问题已记录 ═══")
|
|
1615
|
+
print(f"主题: {record.topic}")
|
|
1616
|
+
print(f"验收条件: {record.criterion_id}")
|
|
1617
|
+
print(f"程序记录: {record.record_id}")
|
|
1618
|
+
if record.result != "passed":
|
|
1619
|
+
print_next_step("先调查问题并和用户确认处理方式,再由用户决定是否 workflow return")
|
|
1620
|
+
return
|
|
1621
|
+
if acceptance_records_mod.topic_records_complete(project_root, wf_state, record.topic):
|
|
1622
|
+
result_path = topic_mod.topic_paths(project_root, record.topic)["acceptance_result"]
|
|
1623
|
+
print_next_step(f"生成或复核 `{result_path}`,再继续其他可验收主题")
|
|
1624
|
+
else:
|
|
1625
|
+
print_next_step("继续展示当前主题尚未确认的人工验收条件")
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
def _stage_index_map(wf_state: state_mod.WorkflowState) -> dict[str, int]:
|
|
1629
|
+
return {stage_name: index for index, stage_name in enumerate(wf_state.stage_path)}
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def _clear_topic_test_state(
|
|
1633
|
+
project_root: str,
|
|
1634
|
+
wf_state: state_mod.WorkflowState,
|
|
1635
|
+
topics: list[str],
|
|
1636
|
+
) -> None:
|
|
1637
|
+
stage_state = wf_state.stages.get("test_execution")
|
|
1638
|
+
if stage_state is not None:
|
|
1639
|
+
for topic in topics:
|
|
1640
|
+
if topic in stage_state.test_tasks:
|
|
1641
|
+
del stage_state.test_tasks[topic]
|
|
1642
|
+
for topic in topics:
|
|
1643
|
+
paths = topic_mod.topic_paths(project_root, topic)
|
|
1644
|
+
result_path = os.path.join(project_root, paths["test_result"])
|
|
1645
|
+
if os.path.exists(result_path):
|
|
1646
|
+
os.remove(result_path)
|
|
1647
|
+
acceptance_records_mod.clear_topic_records(project_root, wf_state, topics)
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def cmd_return(args) -> None:
|
|
1651
|
+
"""用户确认后把当前工作流退回指定阶段,并只清理直接受影响主题的当前状态。
|
|
1652
|
+
|
|
1653
|
+
程序只验证目标是本轮实际路径中当前环节之前的真实环节;不从原因文字猜目标,
|
|
1654
|
+
不根据主题依赖自动扩大失效范围。
|
|
1655
|
+
"""
|
|
1656
|
+
project_root = resolve_project_root()
|
|
1657
|
+
if project_root is None:
|
|
1658
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
1659
|
+
sys.exit(1)
|
|
1660
|
+
wf_state = _load_active_workflow_for_command(project_root)
|
|
1661
|
+
stage_indexes = _stage_index_map(wf_state)
|
|
1662
|
+
if args.to not in stage_indexes:
|
|
1663
|
+
print(f"错误:目标阶段不在当前工作流的实际路径中: {args.to}")
|
|
1664
|
+
print(f"本轮路径: {' → '.join(wf_state.stage_path)}")
|
|
1665
|
+
return
|
|
1666
|
+
if wf_state.current_stage not in stage_indexes or stage_indexes[args.to] >= stage_indexes[wf_state.current_stage]:
|
|
1667
|
+
print(f"错误:只能退回当前阶段之前的阶段,当前是 {stage_label(wf_state.current_stage)}")
|
|
1668
|
+
return
|
|
1669
|
+
if not args.reason.strip():
|
|
1670
|
+
print("错误:必须说明为什么退回")
|
|
1671
|
+
return
|
|
1672
|
+
|
|
1673
|
+
# 直接受影响主题必须明确:已有主题时不允许含糊范围,也不自动扩大
|
|
1674
|
+
if args.topic and args.all_topics:
|
|
1675
|
+
print("错误:--topic 和 --all-topics 互斥,只能选择一种方式说明受影响主题")
|
|
1676
|
+
return
|
|
1677
|
+
if wf_state.topics:
|
|
1678
|
+
if args.all_topics:
|
|
1679
|
+
affected_topics = list(wf_state.topics)
|
|
1680
|
+
elif args.topic:
|
|
1681
|
+
affected_topics = list(dict.fromkeys(args.topic))
|
|
1682
|
+
else:
|
|
1683
|
+
print("错误:当前工作流已有验收主题,必须明确写出直接受影响的主题:")
|
|
1684
|
+
print(" 逐个使用 --topic <主题名称>(可重复),或使用 --all-topics 表示全部主题")
|
|
1685
|
+
print(" 程序不会根据主题依赖自动扩大范围;只有确有影响证据的主题才应列出")
|
|
1686
|
+
return
|
|
1687
|
+
unknown = sorted(set(affected_topics) - set(wf_state.topics))
|
|
1688
|
+
if unknown:
|
|
1689
|
+
print(f"错误:受影响主题不属于当前工作流: {unknown}")
|
|
1690
|
+
return
|
|
1691
|
+
else:
|
|
1692
|
+
# 主题尚未形成(早期阶段):不伪造主题参数
|
|
1693
|
+
affected_topics = []
|
|
1694
|
+
|
|
1695
|
+
# 先确认追踪表能够完成退回更新,再删除测试/验收结果。
|
|
1696
|
+
# 否则追踪表错误会留下“state 仍在原阶段、结果文件却已经被删”的半完成状态。
|
|
1697
|
+
trace_detail = ""
|
|
1698
|
+
if affected_topics:
|
|
1699
|
+
try:
|
|
1700
|
+
trace_detail = traceability_mod.reset_topics_for_return(
|
|
1701
|
+
project_root,
|
|
1702
|
+
wf_state.workflow_id,
|
|
1703
|
+
affected_topics,
|
|
1704
|
+
args.to,
|
|
1705
|
+
)
|
|
1706
|
+
except ValueError as exc:
|
|
1707
|
+
print("═══ 工作流退回失败 ═══")
|
|
1708
|
+
print(f"详情: {exc}")
|
|
1709
|
+
return
|
|
1710
|
+
|
|
1711
|
+
previous_stage = wf_state.current_stage
|
|
1712
|
+
target_index = stage_indexes[args.to]
|
|
1713
|
+
downstream_names = wf_state.stage_path[target_index:]
|
|
1714
|
+
for stage_name in downstream_names:
|
|
1715
|
+
stage_state = wf_state.stages[stage_name]
|
|
1716
|
+
verification_mod.clear_stage_gates(stage_state)
|
|
1717
|
+
stage_state.status = "pending"
|
|
1718
|
+
wf_state.current_stage = args.to
|
|
1719
|
+
wf_state.stages[args.to].status = "in_progress"
|
|
1720
|
+
verification_mod.set_recovery_context(
|
|
1721
|
+
wf_state,
|
|
1722
|
+
args.to,
|
|
1723
|
+
downstream_names,
|
|
1724
|
+
args.reason.strip(),
|
|
1725
|
+
)
|
|
1726
|
+
wf_state.recovery.return_target = args.to
|
|
1727
|
+
wf_state.recovery.affected_topics = affected_topics
|
|
1728
|
+
if ensure_impl_recovery_baseline(project_root, wf_state):
|
|
1729
|
+
state_mod.save_state(project_root, wf_state)
|
|
1730
|
+
|
|
1731
|
+
def stage_index(name: str) -> int:
|
|
1732
|
+
return stage_indexes.get(name, len(wf_state.stage_path))
|
|
1733
|
+
|
|
1734
|
+
# 各目标的精确失效内容:只清确实失效的状态,独立主题的有效记录保留
|
|
1735
|
+
if target_index <= stage_index("spike"):
|
|
1736
|
+
# 从后续环节返回穿刺:跳过标记恢复,三道门重新执行
|
|
1737
|
+
wf_state.spike_skipped = False
|
|
1738
|
+
if target_index <= stage_index("acceptance_plan"):
|
|
1739
|
+
wf_state.verification.acceptance_plan_hash = None
|
|
1740
|
+
if target_index <= stage_index("test_plan"):
|
|
1741
|
+
wf_state.verification.test_plan_hash = None
|
|
1742
|
+
if target_index <= stage_index("impl"):
|
|
1743
|
+
wf_state.verification.impl_hash = None
|
|
1744
|
+
if target_index <= stage_index("test_code"):
|
|
1745
|
+
wf_state.verification.test_code_hash = None
|
|
1746
|
+
if target_index <= stage_index("test_execution"):
|
|
1747
|
+
# 返回测试执行或更早:清除受影响主题的测试任务、测试结果和验收记录
|
|
1748
|
+
_clear_topic_test_state(project_root, wf_state, affected_topics)
|
|
1749
|
+
wf_state.verification.test_result_hash = None
|
|
1750
|
+
wf_state.verification.acceptance_result_hash = None
|
|
1751
|
+
elif target_index <= stage_index("topic_acceptance"):
|
|
1752
|
+
# 返回主题验收:保留测试记录,只清受影响主题的验收记录和结果
|
|
1753
|
+
acceptance_records_mod.clear_topic_records(project_root, wf_state, affected_topics)
|
|
1754
|
+
wf_state.verification.acceptance_result_hash = None
|
|
1755
|
+
if target_index <= stage_index("regression_test"):
|
|
1756
|
+
# 返回最终回归或更早:回归必须重新真实执行;主题结果按上面的规则保留或清除
|
|
1757
|
+
wf_state.verification.regression_test_result_hash = None
|
|
1758
|
+
wf_state.regression_test = state_mod.RegressionTestState()
|
|
1759
|
+
# 返回整体验收或最终设计同步:不删除已经通过的回归记录
|
|
1760
|
+
|
|
1761
|
+
journal_mod.append_entry(
|
|
1762
|
+
project_root,
|
|
1763
|
+
"流程退回",
|
|
1764
|
+
"user",
|
|
1765
|
+
workflow_id=wf_state.workflow_id,
|
|
1766
|
+
from_stage=previous_stage,
|
|
1767
|
+
to_stage=args.to,
|
|
1768
|
+
topics=affected_topics,
|
|
1769
|
+
reason=args.reason.strip(),
|
|
1770
|
+
traceability=trace_detail,
|
|
1771
|
+
recovery_created_at=wf_state.recovery.created_at,
|
|
1772
|
+
)
|
|
1773
|
+
state_mod.save_state(project_root, wf_state)
|
|
1774
|
+
print("═══ 工作流已退回 ═══")
|
|
1775
|
+
print(f"来源环节: {stage_label(previous_stage)}")
|
|
1776
|
+
print(f"目标环节: {stage_label(args.to)}")
|
|
1777
|
+
print(f"直接受影响主题: {', '.join(affected_topics) if affected_topics else '(主题尚未形成)'}")
|
|
1778
|
+
print(f"原因: {args.reason.strip()}")
|
|
1779
|
+
if trace_detail:
|
|
1780
|
+
print(trace_detail)
|
|
1781
|
+
print("未列出的独立主题保留当前测试和验收记录。")
|
|
1782
|
+
print_recovery_details(wf_state)
|
|
1783
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
# gate 命令:3 道闸的总入口 + spike --skip
|
|
1787
|
+
# --discuss-done:第 1 道闸(讨论完毕)
|
|
1788
|
+
# 无 flag:第 2 道闸(代码校验 + Verification Invalidation 检查)
|
|
1789
|
+
# --confirmed:第 3 道闸(用户确认 + 推进 + 记录 hash + 设置架构标记)
|
|
1790
|
+
def cmd_gate(args) -> None:
|
|
1791
|
+
# 定位项目根
|
|
1792
|
+
project_root = resolve_project_root()
|
|
1793
|
+
if project_root is None:
|
|
1794
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
1795
|
+
sys.exit(1)
|
|
1796
|
+
|
|
1797
|
+
# 读 state
|
|
1798
|
+
wf_state = state_mod.load_state(project_root)
|
|
1799
|
+
if wf_state is None:
|
|
1800
|
+
print("错误:还没启动工作流")
|
|
1801
|
+
sys.exit(1)
|
|
1802
|
+
# Run 已结束 → 不能 gate
|
|
1803
|
+
if wf_state.run_status != "active":
|
|
1804
|
+
print(f"错误:Run 已 {wf_state.run_status},无法 gate。")
|
|
1805
|
+
sys.exit(1)
|
|
1806
|
+
refuse_if_pending_start_transaction(project_root)
|
|
1807
|
+
if ensure_stage_path_current(project_root, wf_state):
|
|
1808
|
+
state_mod.save_state(project_root, wf_state)
|
|
1809
|
+
if restore_recovery_context_from_journal(project_root, wf_state):
|
|
1810
|
+
state_mod.save_state(project_root, wf_state)
|
|
1811
|
+
clear_completed_material_recovery(project_root, wf_state)
|
|
1812
|
+
if ensure_impl_recovery_baseline(project_root, wf_state):
|
|
1813
|
+
state_mod.save_state(project_root, wf_state)
|
|
1814
|
+
|
|
1815
|
+
# 要过门禁的 stage 名
|
|
1816
|
+
stage_name = args.stage
|
|
1817
|
+
|
|
1818
|
+
# 所有门禁只能操作当前正在进行的 stage,不能跨阶段提前标记或推进
|
|
1819
|
+
if stage_name not in wf_state.stages:
|
|
1820
|
+
print(f"错误:stage '{stage_name}' 不在当前工作流的 stages 里")
|
|
1821
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1822
|
+
sys.exit(1)
|
|
1823
|
+
if stage_name != wf_state.current_stage:
|
|
1824
|
+
print(f"错误:当前 stage 是 {wf_state.current_stage},不能操作 {stage_name} 的门禁")
|
|
1825
|
+
requested_stage = wf_state.stages[stage_name]
|
|
1826
|
+
if requested_stage.gate.user_confirmed:
|
|
1827
|
+
print(
|
|
1828
|
+
f"{stage_label(stage_name)}已经完成;你刚才重复调用了它的门禁,"
|
|
1829
|
+
f"当前应处理 {stage_label(wf_state.current_stage)}。"
|
|
1830
|
+
)
|
|
1831
|
+
print_recovery_details(wf_state)
|
|
1832
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
1833
|
+
sys.exit(1)
|
|
1834
|
+
|
|
1835
|
+
# --rebaseline 是实施阶段的显式基线确认,不得和其它门禁动作合用。
|
|
1836
|
+
if (
|
|
1837
|
+
args.rebaseline
|
|
1838
|
+
or args.prepare_code
|
|
1839
|
+
or args.accept_existing_code
|
|
1840
|
+
or args.accept_existing_test_code
|
|
1841
|
+
) and (
|
|
1842
|
+
args.skip or args.discuss_done or args.confirmed
|
|
1843
|
+
):
|
|
1844
|
+
print(
|
|
1845
|
+
"错误:--rebaseline、--prepare-code、--accept-existing-code 和 "
|
|
1846
|
+
"--accept-existing-test-code 不能和其它 gate 参数同时使用"
|
|
1847
|
+
)
|
|
1848
|
+
sys.exit(1)
|
|
1849
|
+
|
|
1850
|
+
# ── 特殊:--skip(仅 spike)──
|
|
1851
|
+
if args.skip:
|
|
1852
|
+
# --skip 只适用于 spike stage
|
|
1853
|
+
if stage_name != "spike":
|
|
1854
|
+
print(f"错误:--skip 仅适用于 spike stage,不适用于 {stage_name}")
|
|
1855
|
+
sys.exit(1)
|
|
1856
|
+
# 标记 spike 跳过
|
|
1857
|
+
wf_state.spike_skipped = True
|
|
1858
|
+
# 绕过三道门(全设 True)
|
|
1859
|
+
wf_state.stages[stage_name].gate.discussion_complete = True
|
|
1860
|
+
wf_state.stages[stage_name].gate.code_validated = True
|
|
1861
|
+
wf_state.stages[stage_name].gate.user_confirmed = True
|
|
1862
|
+
wf_state.stages[stage_name].status = "done"
|
|
1863
|
+
# 找下一个 stage
|
|
1864
|
+
stage_names = list(wf_state.stages.keys())
|
|
1865
|
+
current_idx = stage_names.index(stage_name)
|
|
1866
|
+
# 有下一个 stage → 推进
|
|
1867
|
+
if current_idx + 1 < len(stage_names):
|
|
1868
|
+
next_stage = stage_names[current_idx + 1]
|
|
1869
|
+
wf_state.current_stage = next_stage
|
|
1870
|
+
wf_state.stages[next_stage].status = "in_progress"
|
|
1871
|
+
# 清理可能存在的临时内容
|
|
1872
|
+
cleaned_paths = clean_spike_tmp(project_root)
|
|
1873
|
+
# 保存 state
|
|
1874
|
+
state_mod.save_state(project_root, wf_state)
|
|
1875
|
+
# 写 journal:spike 跳过
|
|
1876
|
+
journal_mod.append_entry(project_root, "spike 跳过", "workflow.py",
|
|
1877
|
+
cleaned_paths=cleaned_paths)
|
|
1878
|
+
# 写 journal:阶段推进
|
|
1879
|
+
journal_mod.append_entry(project_root, "阶段推进", "workflow.py",
|
|
1880
|
+
from_=stage_name, to=wf_state.current_stage)
|
|
1881
|
+
# 打印跳过信息
|
|
1882
|
+
print(f"═══ {stage_name} 跳过 ═══")
|
|
1883
|
+
print(f"进入 {wf_state.current_stage}")
|
|
1884
|
+
print_next_step(f"调 `workflow discuss` 加载 {wf_state.current_stage} stage 提示词")
|
|
1885
|
+
return
|
|
1886
|
+
|
|
1887
|
+
# 拿 stage 的 gate 状态
|
|
1888
|
+
stage_state = wf_state.stages[stage_name]
|
|
1889
|
+
gate = stage_state.gate
|
|
1890
|
+
|
|
1891
|
+
# 找到当前阶段策略。第一道门需要它确定哪些文件要记录修改前基线。
|
|
1892
|
+
stage_instances = build_stage_path(wf_state.intent, project_root)
|
|
1893
|
+
stage = get_stage_strategy(stage_name, wf_state, stage_instances)
|
|
1894
|
+
if stage is None:
|
|
1895
|
+
print(f"错误:找不到 stage '{stage_name}' 的策略实现")
|
|
1896
|
+
sys.exit(1)
|
|
1897
|
+
|
|
1898
|
+
if stage_name == "topic_acceptance":
|
|
1899
|
+
try:
|
|
1900
|
+
created_records = acceptance_records_mod.ensure_automated_records(
|
|
1901
|
+
project_root,
|
|
1902
|
+
wf_state,
|
|
1903
|
+
)
|
|
1904
|
+
except ValueError as exc:
|
|
1905
|
+
print("═══ topic_acceptance 自动化验收准备失败 ═══")
|
|
1906
|
+
print(f"详情: {exc}")
|
|
1907
|
+
print_next_step("先修正验收计划、测试计划或测试结果,再重新进入主题验收")
|
|
1908
|
+
return
|
|
1909
|
+
for record in created_records:
|
|
1910
|
+
journal_mod.append_entry(
|
|
1911
|
+
project_root,
|
|
1912
|
+
"自动化验收记录",
|
|
1913
|
+
"workflow.py",
|
|
1914
|
+
workflow_id=wf_state.workflow_id,
|
|
1915
|
+
topic=record.topic,
|
|
1916
|
+
criterion_id=record.criterion_id,
|
|
1917
|
+
result=record.result,
|
|
1918
|
+
record_id=record.record_id,
|
|
1919
|
+
test_ids=record.test_ids,
|
|
1920
|
+
)
|
|
1921
|
+
if created_records:
|
|
1922
|
+
state_mod.save_state(project_root, wf_state)
|
|
1923
|
+
|
|
1924
|
+
# ── --prepare-code:保存实施计划所列文件的真实修改前内容 ──
|
|
1925
|
+
if args.prepare_code:
|
|
1926
|
+
if stage_name != "impl":
|
|
1927
|
+
print("错误:--prepare-code 只适用于 impl stage")
|
|
1928
|
+
sys.exit(1)
|
|
1929
|
+
try:
|
|
1930
|
+
detail, paths = rollback_mod.prepare_impl(project_root, wf_state)
|
|
1931
|
+
except ValueError as exc:
|
|
1932
|
+
print("═══ impl 实施前回退基线准备失败 ═══")
|
|
1933
|
+
print(f"详情: {exc}")
|
|
1934
|
+
print_next_step("修正实施计划中的文件路径或代码基线后,重新调 `workflow gate impl --prepare-code`")
|
|
1935
|
+
return
|
|
1936
|
+
state_mod.save_state(project_root, wf_state)
|
|
1937
|
+
journal_mod.append_entry(
|
|
1938
|
+
project_root,
|
|
1939
|
+
"实施前文件回退基线",
|
|
1940
|
+
"workflow.py",
|
|
1941
|
+
workflow_id=wf_state.workflow_id,
|
|
1942
|
+
manifest=wf_state.rollback.manifest_path,
|
|
1943
|
+
manifest_hash=wf_state.rollback.manifest_hash,
|
|
1944
|
+
plan_hash=wf_state.rollback.plan_hash,
|
|
1945
|
+
planned_paths=paths,
|
|
1946
|
+
)
|
|
1947
|
+
print("═══ impl 实施前回退基线已保存 ═══")
|
|
1948
|
+
print(detail)
|
|
1949
|
+
print(f"计划修改文件: {paths}")
|
|
1950
|
+
print_next_step("现在可以按实施计划修改代码;完成实施后记录后调 `workflow gate impl`")
|
|
1951
|
+
return
|
|
1952
|
+
|
|
1953
|
+
try:
|
|
1954
|
+
current_material_hash = compute_stage_material_hash(project_root, stage)
|
|
1955
|
+
except MaterialError as exc:
|
|
1956
|
+
print(f"═══ {stage_label(stage_name)} 材料检查失败 ═══")
|
|
1957
|
+
print(f"详情: {exc}")
|
|
1958
|
+
print_next_step("先恢复缺失或损坏的阶段材料文件,再执行 `workflow discuss` 重新登记清单")
|
|
1959
|
+
sys.exit(1)
|
|
1960
|
+
if (
|
|
1961
|
+
gate.discussion_complete
|
|
1962
|
+
and (
|
|
1963
|
+
(
|
|
1964
|
+
stage_state.discussion_material_hash is not None
|
|
1965
|
+
and stage_state.discussion_material_hash != current_material_hash
|
|
1966
|
+
)
|
|
1967
|
+
or (
|
|
1968
|
+
stage_name == "test_execution"
|
|
1969
|
+
and stage_state.discussion_material_hash is None
|
|
1970
|
+
)
|
|
1971
|
+
)
|
|
1972
|
+
):
|
|
1973
|
+
previous_material_hash = stage_state.discussion_material_hash
|
|
1974
|
+
verification_mod.clear_stage_gates(stage_state)
|
|
1975
|
+
stage_state.discussion_material_hash = None
|
|
1976
|
+
stage_state.status = "in_progress"
|
|
1977
|
+
stage_index = wf_state.stage_path.index(stage_name)
|
|
1978
|
+
verification_mod.set_recovery_context(
|
|
1979
|
+
wf_state,
|
|
1980
|
+
stage_name,
|
|
1981
|
+
wf_state.stage_path[stage_index:],
|
|
1982
|
+
"当前阶段的流程模板或规范已经更新,旧讨论结论必须重新确认",
|
|
1983
|
+
)
|
|
1984
|
+
journal_mod.append_entry(
|
|
1985
|
+
project_root,
|
|
1986
|
+
"阶段材料变化导致讨论失效",
|
|
1987
|
+
"workflow.py",
|
|
1988
|
+
workflow_id=wf_state.workflow_id,
|
|
1989
|
+
stage=stage_name,
|
|
1990
|
+
previous_material_hash=previous_material_hash,
|
|
1991
|
+
current_material_hash=current_material_hash,
|
|
1992
|
+
)
|
|
1993
|
+
if ensure_impl_recovery_baseline(project_root, wf_state):
|
|
1994
|
+
state_mod.save_state(project_root, wf_state)
|
|
1995
|
+
state_mod.save_state(project_root, wf_state)
|
|
1996
|
+
if not args.discuss_done:
|
|
1997
|
+
print(f"═══ {stage_name} 讨论材料已变化 ═══")
|
|
1998
|
+
print("旧的讨论确认已经失效,必须重新阅读当前材料")
|
|
1999
|
+
print_recovery_details(wf_state)
|
|
2000
|
+
print_next_step("先调 `workflow discuss`,阅读更新后的阶段材料")
|
|
2001
|
+
return
|
|
2002
|
+
|
|
2003
|
+
# ── --accept-existing-code:用户确认已有代码就是本次实施结果 ──
|
|
2004
|
+
if args.accept_existing_code:
|
|
2005
|
+
if stage_name != "impl":
|
|
2006
|
+
print("错误:--accept-existing-code 只适用于 impl stage")
|
|
2007
|
+
sys.exit(1)
|
|
2008
|
+
if not gate.discussion_complete:
|
|
2009
|
+
print("错误:必须先通过 `workflow gate impl --discuss-done`,才能确认已有代码")
|
|
2010
|
+
print_next_step("先完成实施计划讨论,再调 `workflow gate impl --discuss-done`")
|
|
2011
|
+
return
|
|
2012
|
+
rollback_ok, rollback_detail, _ = rollback_mod.validate_prepared(
|
|
2013
|
+
project_root,
|
|
2014
|
+
wf_state,
|
|
2015
|
+
)
|
|
2016
|
+
if not rollback_ok:
|
|
2017
|
+
print("═══ impl 既有代码确认失败 ═══")
|
|
2018
|
+
print(f"详情: {rollback_detail}")
|
|
2019
|
+
print_next_step("先调 `workflow gate impl --prepare-code` 保存当前计划对应的回退清单")
|
|
2020
|
+
return
|
|
2021
|
+
valid, detail, _ = stage.validate_implementation_records(project_root, wf_state)
|
|
2022
|
+
if not valid:
|
|
2023
|
+
print("═══ impl 既有代码确认失败 ═══")
|
|
2024
|
+
print(f"详情: {detail}")
|
|
2025
|
+
print_next_step("补齐实施后记录和追踪表后再调 `workflow gate impl --accept-existing-code`")
|
|
2026
|
+
return
|
|
2027
|
+
|
|
2028
|
+
current_hash = compute_non_test_code_snapshot_hash(project_root)
|
|
2029
|
+
previous_hash = stage_state.existing_code_accepted_hash
|
|
2030
|
+
stage_state.code_baseline_hash = current_hash
|
|
2031
|
+
stage_state.existing_code_accepted_hash = current_hash
|
|
2032
|
+
journal_mod.append_entry(
|
|
2033
|
+
project_root,
|
|
2034
|
+
"既有实施代码确认",
|
|
2035
|
+
"user",
|
|
2036
|
+
workflow_id=wf_state.workflow_id,
|
|
2037
|
+
stage=stage_name,
|
|
2038
|
+
previous_existing_code_hash=previous_hash,
|
|
2039
|
+
code_snapshot_hash=current_hash,
|
|
2040
|
+
reason="用户确认当前代码已经是本次需求的实施结果",
|
|
2041
|
+
)
|
|
2042
|
+
state_mod.save_state(project_root, wf_state)
|
|
2043
|
+
print("═══ impl 既有实施代码已确认 ═══")
|
|
2044
|
+
print(f"已确认代码快照: {current_hash}")
|
|
2045
|
+
print_next_step("调 `workflow gate impl` 做实施代码校验")
|
|
2046
|
+
return
|
|
2047
|
+
|
|
2048
|
+
# ── --accept-existing-test-code:用户确认既有测试代码仍覆盖最新测试计划 ──
|
|
2049
|
+
if args.accept_existing_test_code:
|
|
2050
|
+
if stage_name != "test_code":
|
|
2051
|
+
print("错误:--accept-existing-test-code 只适用于 test_code stage")
|
|
2052
|
+
sys.exit(1)
|
|
2053
|
+
if not gate.discussion_complete:
|
|
2054
|
+
print("错误:必须先通过 `workflow gate test_code --discuss-done`,才能确认既有测试代码")
|
|
2055
|
+
print_next_step("先调 `workflow discuss` 阅读测试代码流程规范和代码开发规范")
|
|
2056
|
+
return
|
|
2057
|
+
valid, detail = stage.validate_existing_test_code(project_root)
|
|
2058
|
+
if not valid:
|
|
2059
|
+
print("═══ test_code 既有测试代码确认失败 ═══")
|
|
2060
|
+
print(f"详情: {detail}")
|
|
2061
|
+
print_next_step("修改测试代码后调 `workflow gate test_code`,或补齐确认条件后重试")
|
|
2062
|
+
return
|
|
2063
|
+
current_hash = compute_test_code_snapshot_hash(project_root)
|
|
2064
|
+
previous_hash = stage_state.existing_test_code_accepted_hash
|
|
2065
|
+
stage_state.existing_test_code_accepted_hash = current_hash
|
|
2066
|
+
journal_mod.append_entry(
|
|
2067
|
+
project_root,
|
|
2068
|
+
"既有测试代码确认",
|
|
2069
|
+
"user",
|
|
2070
|
+
workflow_id=wf_state.workflow_id,
|
|
2071
|
+
stage=stage_name,
|
|
2072
|
+
previous_existing_test_code_hash=previous_hash,
|
|
2073
|
+
test_code_snapshot_hash=current_hash,
|
|
2074
|
+
reason="用户确认当前测试代码已经覆盖最新测试计划",
|
|
2075
|
+
)
|
|
2076
|
+
state_mod.save_state(project_root, wf_state)
|
|
2077
|
+
print("═══ test_code 既有测试代码已确认 ═══")
|
|
2078
|
+
print(f"已确认测试代码快照: {current_hash}")
|
|
2079
|
+
print_next_step("调 `workflow gate test_code` 做测试代码校验")
|
|
2080
|
+
return
|
|
2081
|
+
|
|
2082
|
+
# ── --rebaseline:用户确认当前代码作为新的实施前基线 ──
|
|
2083
|
+
if args.rebaseline:
|
|
2084
|
+
if stage_name != "impl":
|
|
2085
|
+
print("错误:--rebaseline 只适用于 impl stage")
|
|
2086
|
+
sys.exit(1)
|
|
2087
|
+
if gate.discussion_complete:
|
|
2088
|
+
print("错误:impl 的讨论已经完成,不能再重设实施前代码基线")
|
|
2089
|
+
print_next_step("按当前 impl 门禁继续,或作废当前 Run 后重新启动工作流")
|
|
2090
|
+
sys.exit(1)
|
|
2091
|
+
if not _has_loaded_stage_materials(project_root, wf_state, stage):
|
|
2092
|
+
print("错误:重设基线前必须先通过 workflow discuss 加载实施阶段的全部材料")
|
|
2093
|
+
print_next_step("先调 `workflow discuss`,阅读实施计划模板、实施流程规范和代码开发规范")
|
|
2094
|
+
return
|
|
2095
|
+
|
|
2096
|
+
previous_hash = stage_state.code_baseline_hash
|
|
2097
|
+
current_hash = compute_non_test_code_snapshot_hash(project_root)
|
|
2098
|
+
stage_state.code_baseline_hash = current_hash
|
|
2099
|
+
stage_state.existing_code_accepted_hash = None
|
|
2100
|
+
journal_mod.append_entry(
|
|
2101
|
+
project_root,
|
|
2102
|
+
"实施代码基线重设",
|
|
2103
|
+
"user",
|
|
2104
|
+
workflow_id=wf_state.workflow_id,
|
|
2105
|
+
stage=stage_name,
|
|
2106
|
+
reason="用户确认当前代码为实施计划确认前的现状基线",
|
|
2107
|
+
previous_code_snapshot_hash=previous_hash,
|
|
2108
|
+
code_snapshot_hash=current_hash,
|
|
2109
|
+
)
|
|
2110
|
+
state_mod.save_state(project_root, wf_state)
|
|
2111
|
+
print(f"═══ {stage_name} 实施前代码基线已重设 ═══")
|
|
2112
|
+
print(f"当前代码基线: {current_hash}")
|
|
2113
|
+
print_next_step("确认实施前计划没有继续修改代码后,调 `workflow gate impl --discuss-done`")
|
|
2114
|
+
return
|
|
2115
|
+
|
|
2116
|
+
# ── 第 1 道闸:--discuss-done ──
|
|
2117
|
+
if args.discuss_done:
|
|
2118
|
+
# 兼容旧状态:第一道门前处理产物路径迁移,并标记缺失的入场基线
|
|
2119
|
+
if stage_name == "spike" and ensure_spike_baseline(project_root, wf_state):
|
|
2120
|
+
state_mod.save_state(project_root, wf_state)
|
|
2121
|
+
if (
|
|
2122
|
+
not gate.discussion_complete
|
|
2123
|
+
and not _has_loaded_stage_materials(project_root, wf_state, stage)
|
|
2124
|
+
):
|
|
2125
|
+
print(f"═══ {stage_name} 讨论完成校验失败 ═══")
|
|
2126
|
+
if stage_name == "impl":
|
|
2127
|
+
print("详情:还没有通过 workflow discuss 加载实施阶段的全部材料")
|
|
2128
|
+
print_next_step("先调 `workflow discuss`,阅读实施计划模板、实施流程规范和代码开发规范")
|
|
2129
|
+
elif stage_name == "test_code":
|
|
2130
|
+
print("详情:还没有通过 workflow discuss 加载测试代码阶段的流程规范和代码开发规范")
|
|
2131
|
+
print_next_step("先调 `workflow discuss`,阅读测试代码流程规范和测试代码开发规范")
|
|
2132
|
+
else:
|
|
2133
|
+
print(f"详情:还没有通过 workflow discuss 加载 {stage_label(stage_name)}的当前全部材料")
|
|
2134
|
+
print_next_step(
|
|
2135
|
+
f"先调 `workflow discuss`,按输出路径逐份阅读 {stage_label(stage_name)}的模板和规范"
|
|
2136
|
+
)
|
|
2137
|
+
return
|
|
2138
|
+
if not gate.discussion_complete:
|
|
2139
|
+
valid, detail = stage.discussion_validate(project_root, wf_state)
|
|
2140
|
+
if not valid:
|
|
2141
|
+
print(f"═══ {stage_name} 讨论完成校验失败 ═══")
|
|
2142
|
+
print(f"详情: {detail}")
|
|
2143
|
+
print_next_step(f"补齐讨论阶段要求后重新调 `workflow gate {stage_name} --discuss-done`")
|
|
2144
|
+
return
|
|
2145
|
+
# 已经标记过了 → 提示;impl 允许在计划调整后重新确认,只更新计划确认哈希
|
|
2146
|
+
if gate.discussion_complete:
|
|
2147
|
+
if stage_name == "impl":
|
|
2148
|
+
try:
|
|
2149
|
+
confirmed_plan_hash = rollback_mod.compute_plan_hash(
|
|
2150
|
+
project_root,
|
|
2151
|
+
wf_state.topics,
|
|
2152
|
+
)
|
|
2153
|
+
except (ValueError, OSError) as exc:
|
|
2154
|
+
print(f"═══ impl 讨论完成校验失败 ═══")
|
|
2155
|
+
print(f"详情: 无法计算当前实施计划哈希:{exc}")
|
|
2156
|
+
return
|
|
2157
|
+
if stage_state.plan_confirmed_hash != confirmed_plan_hash:
|
|
2158
|
+
stage_state.plan_confirmed_hash = confirmed_plan_hash
|
|
2159
|
+
journal_mod.append_entry(
|
|
2160
|
+
project_root,
|
|
2161
|
+
"实施计划重新确认",
|
|
2162
|
+
"user",
|
|
2163
|
+
workflow_id=wf_state.workflow_id,
|
|
2164
|
+
stage=stage_name,
|
|
2165
|
+
plan_confirmed_hash=confirmed_plan_hash,
|
|
2166
|
+
reason="实施计划调整后用户重新确认;首次原内容副本保持不变",
|
|
2167
|
+
)
|
|
2168
|
+
state_mod.save_state(project_root, wf_state)
|
|
2169
|
+
print("═══ impl 实施计划已重新确认 ═══")
|
|
2170
|
+
print("计划确认哈希已更新;重新执行 `workflow gate impl --prepare-code` 补充新路径副本后继续实施")
|
|
2171
|
+
print_next_step("调 `workflow gate impl --prepare-code`(保留已保存的首次原内容,只为新路径补副本)")
|
|
2172
|
+
return
|
|
2173
|
+
print(f"提示:{stage_name} 的讨论已经标记完毕了")
|
|
2174
|
+
else:
|
|
2175
|
+
# 标记讨论完毕
|
|
2176
|
+
gate.discussion_complete = True
|
|
2177
|
+
stage_state.discussion_material_hash = current_material_hash
|
|
2178
|
+
# impl 记录用户确认的实施计划哈希;后续每次准备回退基线都必须匹配它
|
|
2179
|
+
if stage_name == "impl":
|
|
2180
|
+
try:
|
|
2181
|
+
stage_state.plan_confirmed_hash = rollback_mod.compute_plan_hash(
|
|
2182
|
+
project_root,
|
|
2183
|
+
wf_state.topics,
|
|
2184
|
+
)
|
|
2185
|
+
except (ValueError, OSError):
|
|
2186
|
+
stage_state.plan_confirmed_hash = None
|
|
2187
|
+
# 写 journal:门禁讨论完毕
|
|
2188
|
+
journal_mod.append_entry(project_root, "门禁讨论完毕", "user",
|
|
2189
|
+
stage=stage_name, passed=True,
|
|
2190
|
+
plan_confirmed_hash=stage_state.plan_confirmed_hash
|
|
2191
|
+
if stage_name == "impl" else None)
|
|
2192
|
+
# 讨论结束后、开始写文件前记录基线。重复调用不会覆盖原基线。
|
|
2193
|
+
if stage_name == "test_code" and stage_state.test_code_baseline_hash is None:
|
|
2194
|
+
stage_state.test_code_baseline_hash = verification_mod.compute_test_code_snapshot_hash(
|
|
2195
|
+
project_root,
|
|
2196
|
+
)
|
|
2197
|
+
stage_state.non_test_code_baseline_hash = verification_mod.compute_non_test_code_snapshot_hash(
|
|
2198
|
+
project_root,
|
|
2199
|
+
)
|
|
2200
|
+
journal_mod.append_entry(
|
|
2201
|
+
project_root,
|
|
2202
|
+
"测试代码基线",
|
|
2203
|
+
"workflow.py",
|
|
2204
|
+
stage=stage_name,
|
|
2205
|
+
test_code_snapshot_hash=stage_state.test_code_baseline_hash,
|
|
2206
|
+
non_test_code_snapshot_hash=stage_state.non_test_code_baseline_hash,
|
|
2207
|
+
)
|
|
2208
|
+
try:
|
|
2209
|
+
rollback_test_paths = rollback_mod.prepare_test_code_baseline(
|
|
2210
|
+
project_root,
|
|
2211
|
+
wf_state,
|
|
2212
|
+
)
|
|
2213
|
+
except ValueError as exc:
|
|
2214
|
+
gate.discussion_complete = False
|
|
2215
|
+
stage_state.discussion_material_hash = None
|
|
2216
|
+
stage_state.test_code_baseline_hash = None
|
|
2217
|
+
stage_state.non_test_code_baseline_hash = None
|
|
2218
|
+
state_mod.save_state(project_root, wf_state)
|
|
2219
|
+
print("═══ test_code 讨论完成校验失败 ═══")
|
|
2220
|
+
print(f"详情: 无法保存测试代码修改前内容:{exc}")
|
|
2221
|
+
print_next_step("先修复 impl 的回退清单,再重新调 `workflow gate test_code --discuss-done`")
|
|
2222
|
+
return
|
|
2223
|
+
journal_mod.append_entry(
|
|
2224
|
+
project_root,
|
|
2225
|
+
"测试代码回退基线",
|
|
2226
|
+
"workflow.py",
|
|
2227
|
+
workflow_id=wf_state.workflow_id,
|
|
2228
|
+
saved_paths=rollback_test_paths,
|
|
2229
|
+
manifest_hash=wf_state.rollback.manifest_hash,
|
|
2230
|
+
)
|
|
2231
|
+
ensure_stage_artifact_baseline(project_root, wf_state, stage)
|
|
2232
|
+
# 保存 gate 和基线
|
|
2233
|
+
state_mod.save_state(project_root, wf_state)
|
|
2234
|
+
# 打印讨论完毕
|
|
2235
|
+
print(f"═══ {stage_name} 讨论完毕 ═══")
|
|
2236
|
+
print(f"可以写产出文件了")
|
|
2237
|
+
if recovery_instruction(wf_state):
|
|
2238
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
2239
|
+
else:
|
|
2240
|
+
if stage_name == "impl":
|
|
2241
|
+
print_next_step(
|
|
2242
|
+
"实施前计划已经确认。先调 `workflow gate impl --prepare-code` 保存计划修改文件的原内容,"
|
|
2243
|
+
"保存成功后再修改代码"
|
|
2244
|
+
)
|
|
2245
|
+
else:
|
|
2246
|
+
print_next_step(
|
|
2247
|
+
f"写产出文件 {stage_state.artifact_paths}。"
|
|
2248
|
+
f"写完调 `workflow gate {stage_name}`"
|
|
2249
|
+
)
|
|
2250
|
+
return
|
|
2251
|
+
|
|
2252
|
+
# ── 第 2 道闸:无 flag(代码校验)──
|
|
2253
|
+
if not args.confirmed:
|
|
2254
|
+
# 前置检查:discussion_complete 必须为 True
|
|
2255
|
+
if not gate.discussion_complete:
|
|
2256
|
+
print(f"错误:{stage_name} 还没标记讨论完毕,请先调 "
|
|
2257
|
+
f"`workflow gate {stage_name} --discuss-done`")
|
|
2258
|
+
sys.exit(1)
|
|
2259
|
+
|
|
2260
|
+
# 穿刺门2比较设计文档前后变化;旧状态缺少基线时明确标记无法还原
|
|
2261
|
+
if stage_name == "spike" and ensure_spike_baseline(project_root, wf_state):
|
|
2262
|
+
state_mod.save_state(project_root, wf_state)
|
|
2263
|
+
|
|
2264
|
+
# 兼容旧状态:讨论已经完成但没有记录基线时,从当前文件开始记录。
|
|
2265
|
+
# 这样旧文件不能直接通过,必须在记录后再次修改。
|
|
2266
|
+
if ensure_stage_artifact_baseline(project_root, wf_state, stage):
|
|
2267
|
+
state_mod.save_state(project_root, wf_state)
|
|
2268
|
+
|
|
2269
|
+
# Verification Invalidation 检查:上游 hash 是否变化
|
|
2270
|
+
invalidations = verification_mod.check_invalidation(wf_state, project_root)
|
|
2271
|
+
# 有失效 → 清零下游,解释哪些阶段只需复核、哪些结果必须重做
|
|
2272
|
+
if invalidations:
|
|
2273
|
+
ensure_impl_recovery_baseline(project_root, wf_state)
|
|
2274
|
+
# 保存清零后的 state
|
|
2275
|
+
state_mod.save_state(project_root, wf_state)
|
|
2276
|
+
# 写 journal:验证失效
|
|
2277
|
+
for from_stage, to_stages in invalidations:
|
|
2278
|
+
journal_mod.append_entry(
|
|
2279
|
+
project_root,
|
|
2280
|
+
"验证失效",
|
|
2281
|
+
"workflow.py",
|
|
2282
|
+
workflow_id=wf_state.workflow_id,
|
|
2283
|
+
from_stage=from_stage,
|
|
2284
|
+
to_stage=to_stages,
|
|
2285
|
+
reason=wf_state.recovery.reason or "上游内容已变化",
|
|
2286
|
+
recovery_created_at=wf_state.recovery.created_at,
|
|
2287
|
+
)
|
|
2288
|
+
# 打印失效信息
|
|
2289
|
+
print(f"═══ 验证失效 ═══")
|
|
2290
|
+
for from_stage, to_stages in invalidations:
|
|
2291
|
+
print(f" {from_stage} 变化 → 清零 {to_stages}")
|
|
2292
|
+
print_recovery_details(wf_state)
|
|
2293
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
2294
|
+
return
|
|
2295
|
+
|
|
2296
|
+
if stage_name == "test_code":
|
|
2297
|
+
try:
|
|
2298
|
+
changed_test_paths = rollback_mod.finalize_test_code_changes(
|
|
2299
|
+
project_root,
|
|
2300
|
+
wf_state,
|
|
2301
|
+
)
|
|
2302
|
+
except ValueError as exc:
|
|
2303
|
+
print("═══ test_code 回退记录校验失败 ═══")
|
|
2304
|
+
print(f"详情: {exc}")
|
|
2305
|
+
print_next_step("修复测试代码回退记录后再调 `workflow gate test_code`")
|
|
2306
|
+
return
|
|
2307
|
+
state_mod.save_state(project_root, wf_state)
|
|
2308
|
+
journal_mod.append_entry(
|
|
2309
|
+
project_root,
|
|
2310
|
+
"测试代码变化登记",
|
|
2311
|
+
"workflow.py",
|
|
2312
|
+
workflow_id=wf_state.workflow_id,
|
|
2313
|
+
changed_paths=changed_test_paths,
|
|
2314
|
+
manifest_hash=wf_state.rollback.manifest_hash,
|
|
2315
|
+
)
|
|
2316
|
+
|
|
2317
|
+
# 跑 code_validate(第 2 道闸的核心)。
|
|
2318
|
+
if stage_name == "regression_test":
|
|
2319
|
+
print("现在真实执行一次项目全量测试入口;本次执行结果将作为最终回归事实。")
|
|
2320
|
+
passed, details = validate_stage_output(
|
|
2321
|
+
project_root,
|
|
2322
|
+
wf_state,
|
|
2323
|
+
stage_name,
|
|
2324
|
+
stage,
|
|
2325
|
+
)
|
|
2326
|
+
# 写 journal:门禁代码校验
|
|
2327
|
+
journal_mod.append_entry(project_root, "门禁代码校验", "workflow.py",
|
|
2328
|
+
stage=stage_name, passed=passed, details=details)
|
|
2329
|
+
|
|
2330
|
+
# 检查产出文件是否存在(写 journal:产出文件检查)
|
|
2331
|
+
for artifact in stage_state.artifact_paths:
|
|
2332
|
+
full_path = os.path.join(project_root, artifact)
|
|
2333
|
+
exists = os.path.exists(full_path)
|
|
2334
|
+
journal_mod.append_entry(project_root, "产出文件检查", "workflow.py",
|
|
2335
|
+
stage=stage_name, artifact=artifact, exists=exists)
|
|
2336
|
+
|
|
2337
|
+
# 校验不通过
|
|
2338
|
+
if not passed:
|
|
2339
|
+
if (
|
|
2340
|
+
stage_name == "regression_test"
|
|
2341
|
+
and wf_state.intent == "bugfix"
|
|
2342
|
+
and bug_record_mod.has_explicit_regression_failure(
|
|
2343
|
+
project_root,
|
|
2344
|
+
wf_state.workflow_id,
|
|
2345
|
+
)
|
|
2346
|
+
):
|
|
2347
|
+
try:
|
|
2348
|
+
failure_detail = bug_record_mod.record_regression_failure(
|
|
2349
|
+
project_root,
|
|
2350
|
+
wf_state.workflow_id,
|
|
2351
|
+
topic_mod.current_workflow_topics(project_root),
|
|
2352
|
+
)
|
|
2353
|
+
journal_mod.append_entry(
|
|
2354
|
+
project_root,
|
|
2355
|
+
"缺陷状态更新",
|
|
2356
|
+
"workflow.py",
|
|
2357
|
+
stage=stage_name,
|
|
2358
|
+
details=failure_detail,
|
|
2359
|
+
)
|
|
2360
|
+
except ValueError as exc:
|
|
2361
|
+
journal_mod.append_entry(
|
|
2362
|
+
project_root,
|
|
2363
|
+
"缺陷状态更新失败",
|
|
2364
|
+
"workflow.py",
|
|
2365
|
+
stage=stage_name,
|
|
2366
|
+
details=str(exc),
|
|
2367
|
+
)
|
|
2368
|
+
# 之前通过过门2,但产物后来被改坏时,旧的通过标记必须失效
|
|
2369
|
+
gate.code_validated = False
|
|
2370
|
+
gate.user_confirmed = False
|
|
2371
|
+
state_mod.save_state(project_root, wf_state)
|
|
2372
|
+
print(f"═══ {stage_name} 代码校验失败 ═══")
|
|
2373
|
+
print(f"详情: {details}")
|
|
2374
|
+
recovery_command_needed = (
|
|
2375
|
+
"--accept-existing-code" in details
|
|
2376
|
+
or "--accept-existing-test-code" in details
|
|
2377
|
+
)
|
|
2378
|
+
if stage_name == "regression_test":
|
|
2379
|
+
print_next_step(regression_failure_next_step())
|
|
2380
|
+
elif recovery_instruction(wf_state) and recovery_command_needed:
|
|
2381
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
2382
|
+
elif recovery_instruction(wf_state):
|
|
2383
|
+
print_next_step(
|
|
2384
|
+
f"按上面的具体详情修正当前 {stage_label(stage_name)},"
|
|
2385
|
+
f"修正后再调 `workflow gate {stage_name}`"
|
|
2386
|
+
)
|
|
2387
|
+
else:
|
|
2388
|
+
print_next_step(f"产出文件未就绪,补完后再调 `workflow gate {stage_name}`")
|
|
2389
|
+
return
|
|
2390
|
+
|
|
2391
|
+
# 校验通过
|
|
2392
|
+
gate.code_validated = True
|
|
2393
|
+
# 标记产出时间
|
|
2394
|
+
if stage_state.artifact_produced_at is None:
|
|
2395
|
+
stage_state.artifact_produced_at = state_mod.now_iso()
|
|
2396
|
+
# 保存 state
|
|
2397
|
+
state_mod.save_state(project_root, wf_state)
|
|
2398
|
+
# 写 journal:门禁代码校验通过
|
|
2399
|
+
journal_mod.append_entry(project_root, "门禁代码校验", "workflow.py",
|
|
2400
|
+
stage=stage_name, passed=True, details=details)
|
|
2401
|
+
# 打印校验通过
|
|
2402
|
+
print(f"═══ {stage_name} 代码校验通过 ═══")
|
|
2403
|
+
print(f"详情: {details}")
|
|
2404
|
+
print_next_step(confirmation_next_step(stage_name))
|
|
2405
|
+
return
|
|
2406
|
+
|
|
2407
|
+
# ── 第 3 道闸:--confirmed(用户确认 + 推进)──
|
|
2408
|
+
# 前置检查:code_validated 必须为 True
|
|
2409
|
+
if not gate.code_validated:
|
|
2410
|
+
print(f"错误:{stage_name} 还没跑代码校验,请先调 "
|
|
2411
|
+
f"`workflow gate {stage_name}`")
|
|
2412
|
+
sys.exit(1)
|
|
2413
|
+
|
|
2414
|
+
# 门2通过后文件仍可能变化。门3推进前必须重新检查当前文件,不能只相信旧布尔值。
|
|
2415
|
+
invalidations = verification_mod.check_invalidation(wf_state, project_root)
|
|
2416
|
+
if invalidations:
|
|
2417
|
+
ensure_impl_recovery_baseline(project_root, wf_state)
|
|
2418
|
+
state_mod.save_state(project_root, wf_state)
|
|
2419
|
+
for from_stage, to_stages in invalidations:
|
|
2420
|
+
journal_mod.append_entry(
|
|
2421
|
+
project_root,
|
|
2422
|
+
"验证失效",
|
|
2423
|
+
"workflow.py",
|
|
2424
|
+
workflow_id=wf_state.workflow_id,
|
|
2425
|
+
from_stage=from_stage,
|
|
2426
|
+
to_stage=to_stages,
|
|
2427
|
+
reason=wf_state.recovery.reason or "用户确认前发现上游内容已变化",
|
|
2428
|
+
recovery_created_at=wf_state.recovery.created_at,
|
|
2429
|
+
)
|
|
2430
|
+
print("═══ 用户确认前校验失败 ═══")
|
|
2431
|
+
for from_stage, to_stages in invalidations:
|
|
2432
|
+
print(f" {from_stage} 变化 → 清零 {to_stages}")
|
|
2433
|
+
print_recovery_details(wf_state)
|
|
2434
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
2435
|
+
return
|
|
2436
|
+
|
|
2437
|
+
passed, details = validate_stage_output(
|
|
2438
|
+
project_root,
|
|
2439
|
+
wf_state,
|
|
2440
|
+
stage_name,
|
|
2441
|
+
stage,
|
|
2442
|
+
execute_regression=False,
|
|
2443
|
+
)
|
|
2444
|
+
journal_mod.append_entry(
|
|
2445
|
+
project_root,
|
|
2446
|
+
"门禁确认前复核",
|
|
2447
|
+
"workflow.py",
|
|
2448
|
+
stage=stage_name,
|
|
2449
|
+
passed=passed,
|
|
2450
|
+
details=details,
|
|
2451
|
+
)
|
|
2452
|
+
if not passed:
|
|
2453
|
+
gate.code_validated = False
|
|
2454
|
+
gate.user_confirmed = False
|
|
2455
|
+
state_mod.save_state(project_root, wf_state)
|
|
2456
|
+
print(f"═══ {stage_name} 用户确认前校验失败 ═══")
|
|
2457
|
+
print(f"详情: {details}")
|
|
2458
|
+
if stage_name == "regression_test":
|
|
2459
|
+
print_next_step(regression_failure_next_step())
|
|
2460
|
+
else:
|
|
2461
|
+
print_next_step(f"修正文档后重新调 `workflow gate {stage_name}`")
|
|
2462
|
+
return
|
|
2463
|
+
|
|
2464
|
+
try:
|
|
2465
|
+
added_file_keys = _register_stage_artifact_keys(
|
|
2466
|
+
project_root,
|
|
2467
|
+
wf_state,
|
|
2468
|
+
stage_name,
|
|
2469
|
+
)
|
|
2470
|
+
except (OSError, ValueError) as exc:
|
|
2471
|
+
print(f"═══ {stage_name} 正式文件标识登记失败 ═══")
|
|
2472
|
+
print(f"详情: {exc}")
|
|
2473
|
+
print_next_step("修正正式文档标题或文件名后重新执行当前环节第二道门")
|
|
2474
|
+
return
|
|
2475
|
+
if added_file_keys:
|
|
2476
|
+
journal_mod.append_entry(
|
|
2477
|
+
project_root,
|
|
2478
|
+
"正式文件标识登记",
|
|
2479
|
+
"workflow.py",
|
|
2480
|
+
workflow_id=wf_state.workflow_id,
|
|
2481
|
+
stage=stage_name,
|
|
2482
|
+
added=added_file_keys,
|
|
2483
|
+
)
|
|
2484
|
+
|
|
2485
|
+
# 修 bug 在 reproduce(缺陷复现)确认时确定并登记验收主题。
|
|
2486
|
+
if stage_name == "reproduce" and wf_state.intent == "bugfix":
|
|
2487
|
+
topics = topic_mod.list_reproduce_topics(project_root, wf_state.workflow_id)
|
|
2488
|
+
if not topics:
|
|
2489
|
+
print("错误:缺陷复现记录没有验收主题")
|
|
2490
|
+
print_next_step("在 bug/<缺陷记录>.md 中补充验收主题后重新执行缺陷复现门禁")
|
|
2491
|
+
return
|
|
2492
|
+
try:
|
|
2493
|
+
project_mod.register_topics(project_root, topics)
|
|
2494
|
+
except ValueError as exc:
|
|
2495
|
+
print(f"错误:{exc}")
|
|
2496
|
+
print_next_step("修改重复的主题名称后重新执行缺陷复现门禁")
|
|
2497
|
+
return
|
|
2498
|
+
wf_state.topics = topics
|
|
2499
|
+
wf_state.topic = topics[0] if len(topics) == 1 else None
|
|
2500
|
+
journal_mod.append_entry(
|
|
2501
|
+
project_root,
|
|
2502
|
+
"主题确定",
|
|
2503
|
+
"user",
|
|
2504
|
+
stage="reproduce",
|
|
2505
|
+
topics=topics,
|
|
2506
|
+
)
|
|
2507
|
+
|
|
2508
|
+
# acceptance_plan(验收计划)确认时,从计划文件名确定本次全部主题并登记历史。
|
|
2509
|
+
# bugfix 主题已经在 reproduce 确认,这里只复核,不再登记或新增。
|
|
2510
|
+
elif stage_name == "acceptance_plan":
|
|
2511
|
+
if wf_state.intent == "bugfix":
|
|
2512
|
+
topics = topic_mod.list_acceptance_index_topics(
|
|
2513
|
+
project_root,
|
|
2514
|
+
wf_state.workflow_id,
|
|
2515
|
+
)
|
|
2516
|
+
if not topics:
|
|
2517
|
+
print("错误:修 bug 的验收主题必须先在缺陷复现阶段确定")
|
|
2518
|
+
print_next_step("返回 reproduce 阶段补充验收主题")
|
|
2519
|
+
return
|
|
2520
|
+
else:
|
|
2521
|
+
topics = topic_mod.list_acceptance_index_topics(
|
|
2522
|
+
project_root,
|
|
2523
|
+
wf_state.workflow_id,
|
|
2524
|
+
)
|
|
2525
|
+
if not topics:
|
|
2526
|
+
print(f"错误:没有找到 {artifact_paths_mod.ACCEPTANCE_INDEX_DOC} 中的本次验收主题")
|
|
2527
|
+
print_next_step(
|
|
2528
|
+
f"补充 `{artifact_paths_mod.ACCEPTANCE_INDEX_DOC}` 和 "
|
|
2529
|
+
"`acceptance/<主题文件标识>_验收计划.md` 后重新执行验收计划门禁"
|
|
2530
|
+
)
|
|
2531
|
+
return
|
|
2532
|
+
if wf_state.intent != "bugfix":
|
|
2533
|
+
previous_topics = set(wf_state.topics or ([wf_state.topic] if wf_state.topic else []))
|
|
2534
|
+
new_topics = [topic for topic in topics if topic not in previous_topics]
|
|
2535
|
+
try:
|
|
2536
|
+
project_mod.register_topics(project_root, new_topics)
|
|
2537
|
+
except ValueError as exc:
|
|
2538
|
+
print(f"错误:{exc}")
|
|
2539
|
+
print_next_step("修改重复的主题名称后重新执行验收计划门禁")
|
|
2540
|
+
return
|
|
2541
|
+
wf_state.topics = topics
|
|
2542
|
+
wf_state.topic = topics[0] if len(topics) == 1 else None
|
|
2543
|
+
journal_mod.append_entry(
|
|
2544
|
+
project_root,
|
|
2545
|
+
"主题确定",
|
|
2546
|
+
"user",
|
|
2547
|
+
topics=topics,
|
|
2548
|
+
)
|
|
2549
|
+
|
|
2550
|
+
# 只有测试代码的第三道门确认通过后,才保存可供后续实施复用的状态。
|
|
2551
|
+
if stage_name == "test_code":
|
|
2552
|
+
try:
|
|
2553
|
+
accepted_test_paths = rollback_mod.accept_test_code_inventory(
|
|
2554
|
+
project_root,
|
|
2555
|
+
wf_state,
|
|
2556
|
+
)
|
|
2557
|
+
except ValueError as exc:
|
|
2558
|
+
gate.user_confirmed = False
|
|
2559
|
+
state_mod.save_state(project_root, wf_state)
|
|
2560
|
+
print("═══ test_code 确认状态保存失败 ═══")
|
|
2561
|
+
print(f"详情: {exc}")
|
|
2562
|
+
print_next_step("修复测试代码回退记录后重新执行当前确认门")
|
|
2563
|
+
return
|
|
2564
|
+
journal_mod.append_entry(
|
|
2565
|
+
project_root,
|
|
2566
|
+
"已确认测试文件状态",
|
|
2567
|
+
"user",
|
|
2568
|
+
workflow_id=wf_state.workflow_id,
|
|
2569
|
+
paths=accepted_test_paths,
|
|
2570
|
+
manifest_hash=wf_state.rollback.manifest_hash,
|
|
2571
|
+
)
|
|
2572
|
+
|
|
2573
|
+
# 阶段确认前先写入固定的追踪表和缺陷状态;更新失败时不推进阶段。
|
|
2574
|
+
try:
|
|
2575
|
+
apply_stage_completion_updates(
|
|
2576
|
+
project_root,
|
|
2577
|
+
wf_state,
|
|
2578
|
+
stage_name,
|
|
2579
|
+
)
|
|
2580
|
+
except ValueError as exc:
|
|
2581
|
+
gate.user_confirmed = False
|
|
2582
|
+
state_mod.save_state(project_root, wf_state)
|
|
2583
|
+
print(f"═══ {stage_name} 固定记录更新失败 ═══")
|
|
2584
|
+
print(f"详情: {exc}")
|
|
2585
|
+
print_next_step(f"补齐固定记录后重新调 `workflow gate {stage_name} --confirmed`")
|
|
2586
|
+
return
|
|
2587
|
+
|
|
2588
|
+
# 标记用户确认
|
|
2589
|
+
gate.user_confirmed = True
|
|
2590
|
+
# stage 状态改为 done
|
|
2591
|
+
stage_state.status = "done"
|
|
2592
|
+
# 模板/规范变更触发的恢复只解释触发阶段;该阶段重新确认后不再向后续阶段显示旧原因。
|
|
2593
|
+
clear_completed_material_recovery(project_root, wf_state)
|
|
2594
|
+
|
|
2595
|
+
# 调 on_advance 钩子(spike 清理临时代码、样本和原始输出)
|
|
2596
|
+
cleaned_paths = stage.on_advance(project_root)
|
|
2597
|
+
if stage_name == "spike":
|
|
2598
|
+
journal_mod.append_entry(
|
|
2599
|
+
project_root,
|
|
2600
|
+
"spike 清理",
|
|
2601
|
+
"workflow.py",
|
|
2602
|
+
cleaned_paths=cleaned_paths,
|
|
2603
|
+
)
|
|
2604
|
+
|
|
2605
|
+
# ── 记录 verification hash(验证绑定哈希)──
|
|
2606
|
+
# impl 完成后绑定实施代码和实施记录;后续测试阶段只绑定自己的结果。
|
|
2607
|
+
if stage_name == "impl":
|
|
2608
|
+
wf_state.verification.impl_hash = verification_mod.compute_impl_hash(project_root, wf_state.topics)
|
|
2609
|
+
# 实施代码变化后,测试代码、测试执行、主题验收和后续阶段必须重做。
|
|
2610
|
+
for sn in ["test_code", "test_execution", "topic_acceptance", "regression_test", "overall_acceptance"]:
|
|
2611
|
+
if sn in wf_state.stages:
|
|
2612
|
+
verification_mod.clear_stage_gates(wf_state.stages[sn])
|
|
2613
|
+
wf_state.verification.test_result_hash = None
|
|
2614
|
+
wf_state.verification.acceptance_result_hash = None
|
|
2615
|
+
wf_state.verification.test_code_hash = None
|
|
2616
|
+
# test_plan stage → 记录 test_plan_hash
|
|
2617
|
+
elif stage_name == "test_plan":
|
|
2618
|
+
wf_state.verification.test_plan_hash = verification_mod.compute_test_plan_hash(project_root, wf_state.topics)
|
|
2619
|
+
wf_state.verification.test_code_hash = None
|
|
2620
|
+
wf_state.verification.test_result_hash = None
|
|
2621
|
+
wf_state.verification.acceptance_result_hash = None
|
|
2622
|
+
# acceptance_plan stage → 记录 acceptance_plan_hash,退 test_plan 待检查
|
|
2623
|
+
elif stage_name == "acceptance_plan":
|
|
2624
|
+
wf_state.verification.acceptance_plan_hash = verification_mod.compute_acceptance_plan_hash(project_root, wf_state.topics)
|
|
2625
|
+
wf_state.verification.test_code_hash = None
|
|
2626
|
+
wf_state.verification.test_result_hash = None
|
|
2627
|
+
wf_state.verification.acceptance_result_hash = None
|
|
2628
|
+
# acceptance_plan 变了 → test_plan 需要重新检查
|
|
2629
|
+
if "test_plan" in wf_state.stages:
|
|
2630
|
+
wf_state.stages["test_plan"].gate.code_validated = False
|
|
2631
|
+
wf_state.stages["test_plan"].gate.user_confirmed = False
|
|
2632
|
+
wf_state.stages["test_plan"].status = "pending"
|
|
2633
|
+
# test_code stage → 冻结确认后的测试代码、测试配置和统一测试入口。
|
|
2634
|
+
elif stage_name == "test_code":
|
|
2635
|
+
wf_state.verification.test_code_hash = verification_mod.compute_test_code_snapshot_hash(
|
|
2636
|
+
project_root
|
|
2637
|
+
)
|
|
2638
|
+
wf_state.verification.test_result_hash = None
|
|
2639
|
+
wf_state.verification.acceptance_result_hash = None
|
|
2640
|
+
# test_execution stage → 记录 test_result_hash
|
|
2641
|
+
elif stage_name == "test_execution":
|
|
2642
|
+
wf_state.verification.test_result_hash = verification_mod.compute_test_result_hash(project_root, wf_state.topics)
|
|
2643
|
+
wf_state.verification.acceptance_result_hash = None
|
|
2644
|
+
# topic_acceptance stage → 记录 acceptance_result_hash
|
|
2645
|
+
elif stage_name == "topic_acceptance":
|
|
2646
|
+
wf_state.verification.acceptance_result_hash = verification_mod.compute_acceptance_result_hash(
|
|
2647
|
+
project_root,
|
|
2648
|
+
wf_state.topics,
|
|
2649
|
+
)
|
|
2650
|
+
elif stage_name == "regression_test":
|
|
2651
|
+
wf_state.verification.regression_test_result_hash = (
|
|
2652
|
+
verification_mod.compute_regression_test_result_hash(project_root)
|
|
2653
|
+
)
|
|
2654
|
+
|
|
2655
|
+
# ── 设置 Architecture Gate Marks ──
|
|
2656
|
+
# 前段架构 stage → preliminary_done
|
|
2657
|
+
if stage_name in ("code_design", "revise_code_design", "project_design_init"):
|
|
2658
|
+
wf_state.architecture.preliminary_done = True
|
|
2659
|
+
journal_mod.append_entry(project_root, "架构标记", "workflow.py",
|
|
2660
|
+
mark="preliminary_done", stage=stage_name)
|
|
2661
|
+
# 末段架构 stage → detailed_done
|
|
2662
|
+
if stage_name == "update_code_design":
|
|
2663
|
+
wf_state.architecture.detailed_done = True
|
|
2664
|
+
journal_mod.append_entry(project_root, "架构标记", "workflow.py",
|
|
2665
|
+
mark="detailed_done", stage=stage_name)
|
|
2666
|
+
|
|
2667
|
+
# ── 设置 project_design_initialized ──
|
|
2668
|
+
# project_design_init 完成 → 置 true
|
|
2669
|
+
if stage_name == "project_design_init":
|
|
2670
|
+
project_mod.set_project_design_initialized(project_root, True)
|
|
2671
|
+
# from_scratch 的 code_design 完成 + spec 也完成 → 置 true
|
|
2672
|
+
elif stage_name == "code_design" and wf_state.intent == "from_scratch":
|
|
2673
|
+
if "spec" in wf_state.stages and wf_state.stages["spec"].gate.user_confirmed:
|
|
2674
|
+
project_mod.set_project_design_initialized(project_root, True)
|
|
2675
|
+
# from_scratch 的 spec 完成 + code_design 也完成 → 置 true
|
|
2676
|
+
elif stage_name == "spec" and wf_state.intent == "from_scratch":
|
|
2677
|
+
if "code_design" in wf_state.stages and wf_state.stages["code_design"].gate.user_confirmed:
|
|
2678
|
+
project_mod.set_project_design_initialized(project_root, True)
|
|
2679
|
+
|
|
2680
|
+
# 写 journal:门禁用户确认
|
|
2681
|
+
journal_mod.append_entry(project_root, "门禁用户确认", "user",
|
|
2682
|
+
stage=stage_name, passed=True)
|
|
2683
|
+
|
|
2684
|
+
# 找下一个 stage
|
|
2685
|
+
stage_names = list(wf_state.stages.keys())
|
|
2686
|
+
current_idx = stage_names.index(stage_name)
|
|
2687
|
+
|
|
2688
|
+
# 有下一个 stage → 推进
|
|
2689
|
+
if current_idx + 1 < len(stage_names):
|
|
2690
|
+
next_stage = stage_names[current_idx + 1]
|
|
2691
|
+
wf_state.current_stage = next_stage
|
|
2692
|
+
wf_state.stages[next_stage].status = "in_progress"
|
|
2693
|
+
# 新流程在真正进入 spike 时记录设计基线
|
|
2694
|
+
if next_stage == "spike":
|
|
2695
|
+
ensure_spike_baseline(project_root, wf_state, capture_if_missing=True)
|
|
2696
|
+
if next_stage == "impl":
|
|
2697
|
+
wf_state.stages[next_stage].code_baseline_hash = compute_non_test_code_snapshot_hash(project_root)
|
|
2698
|
+
journal_mod.append_entry(
|
|
2699
|
+
project_root,
|
|
2700
|
+
"实施代码基线",
|
|
2701
|
+
"workflow.py",
|
|
2702
|
+
stage=next_stage,
|
|
2703
|
+
code_snapshot_hash=wf_state.stages[next_stage].code_baseline_hash,
|
|
2704
|
+
)
|
|
2705
|
+
# 保存 state
|
|
2706
|
+
state_mod.save_state(project_root, wf_state)
|
|
2707
|
+
# 写 journal:阶段推进
|
|
2708
|
+
journal_mod.append_entry(project_root, "阶段推进", "workflow.py",
|
|
2709
|
+
from_=stage_name, to=next_stage)
|
|
2710
|
+
# 打印完成 + 进入下一 stage
|
|
2711
|
+
print(f"═══ {stage_name} 完成 ═══")
|
|
2712
|
+
print(f"进入 {next_stage}")
|
|
2713
|
+
# 下一步:discuss 下一 stage
|
|
2714
|
+
print_recovery_details(wf_state)
|
|
2715
|
+
print_next_step(current_stage_next_instruction(wf_state))
|
|
2716
|
+
# 没有下一个 stage → 所有 stage 已完成
|
|
2717
|
+
else:
|
|
2718
|
+
# 临时置 "completed",由 done 命令确认
|
|
2719
|
+
wf_state.current_stage = "completed"
|
|
2720
|
+
# 保存 state
|
|
2721
|
+
state_mod.save_state(project_root, wf_state)
|
|
2722
|
+
# 写 journal:阶段推进到 completed
|
|
2723
|
+
journal_mod.append_entry(project_root, "阶段推进", "workflow.py",
|
|
2724
|
+
from_=stage_name, to="completed")
|
|
2725
|
+
# 打印完成
|
|
2726
|
+
print(f"═══ {stage_name} 完成 ═══")
|
|
2727
|
+
print(f"所有 stage 已完成")
|
|
2728
|
+
# 下一步:done
|
|
2729
|
+
print_next_step("调 `workflow done` 标记完成")
|
|
2730
|
+
|
|
2731
|
+
|
|
2732
|
+
# status 命令:打印 state + journal 摘要;旧状态会先迁移到当前阶段顺序
|
|
2733
|
+
def cmd_status(args) -> None:
|
|
2734
|
+
# 定位项目根
|
|
2735
|
+
project_root = resolve_project_root()
|
|
2736
|
+
if project_root is None:
|
|
2737
|
+
print("找不到 .workflow_loop/ 目录。请先在项目根执行官方安装脚本。")
|
|
2738
|
+
return
|
|
2739
|
+
refuse_if_pending_start_transaction(project_root)
|
|
2740
|
+
|
|
2741
|
+
# 读 state
|
|
2742
|
+
wf_state = state_mod.load_state(project_root)
|
|
2743
|
+
if wf_state is None:
|
|
2744
|
+
print("还没启动工作流。调 `workflow start` 查看可选意图。")
|
|
2745
|
+
return
|
|
2746
|
+
if wf_state.run_status == "active" and ensure_stage_path_current(project_root, wf_state):
|
|
2747
|
+
state_mod.save_state(project_root, wf_state)
|
|
2748
|
+
if restore_recovery_context_from_journal(project_root, wf_state):
|
|
2749
|
+
state_mod.save_state(project_root, wf_state)
|
|
2750
|
+
clear_completed_material_recovery(project_root, wf_state)
|
|
2751
|
+
if ensure_impl_recovery_baseline(project_root, wf_state):
|
|
2752
|
+
state_mod.save_state(project_root, wf_state)
|
|
2753
|
+
|
|
2754
|
+
# 打印 state 摘要
|
|
2755
|
+
print(f"═══ 工作流状态 ═══")
|
|
2756
|
+
print(f"workflow_id: {wf_state.workflow_id}")
|
|
2757
|
+
print(f"intent: {wf_state.intent}")
|
|
2758
|
+
print(f"run_status: {wf_state.run_status}")
|
|
2759
|
+
print(f"当前 stage: {wf_state.current_stage}")
|
|
2760
|
+
print(f"主题: {wf_state.topics or '(未定)'}")
|
|
2761
|
+
print(f"启动时间: {wf_state.started_at}")
|
|
2762
|
+
print(f"结束时间: {wf_state.ended_at or '(未完成)'}")
|
|
2763
|
+
print(f"作废时间: {wf_state.aborted_at or '(未作废)'}")
|
|
2764
|
+
print(f"spike_skipped: {wf_state.spike_skipped}")
|
|
2765
|
+
print(f"架构: preliminary_done={wf_state.architecture.preliminary_done}, detailed_done={wf_state.architecture.detailed_done}")
|
|
2766
|
+
if wf_state.rollback.manifest_path:
|
|
2767
|
+
print(
|
|
2768
|
+
"实施前回退基线: 已准备 "
|
|
2769
|
+
f"({wf_state.rollback.manifest_path},计划文件 {wf_state.rollback.planned_paths})"
|
|
2770
|
+
)
|
|
2771
|
+
else:
|
|
2772
|
+
print("实施前回退基线: 未准备")
|
|
2773
|
+
if verification_mod.recovery_summary(wf_state):
|
|
2774
|
+
print("\n当前处于上游变化后的恢复流程:")
|
|
2775
|
+
print_recovery_details(wf_state)
|
|
2776
|
+
# 打印每个 stage 的门禁状态
|
|
2777
|
+
print(f"\n各阶段门禁状态:")
|
|
2778
|
+
for name, stage_state in wf_state.stages.items():
|
|
2779
|
+
gate = stage_state.gate
|
|
2780
|
+
# 3 道闸用 ✓/✗ 显示
|
|
2781
|
+
d = "✓" if gate.discussion_complete else "✗"
|
|
2782
|
+
c = "✓" if gate.code_validated else "✗"
|
|
2783
|
+
u = "✓" if gate.user_confirmed else "✗"
|
|
2784
|
+
# 当前 stage 用 → 标记
|
|
2785
|
+
marker = "→" if name == wf_state.current_stage else " "
|
|
2786
|
+
print(f" {marker} {name:20s} [{stage_state.status:12s}] 讨论:{d} 校验:{c} 确认:{u}")
|
|
2787
|
+
# 打印 journal 最近 10 条
|
|
2788
|
+
print(f"\n最近 journal 记录:")
|
|
2789
|
+
recent = journal_mod.read_recent(project_root, count=10)
|
|
2790
|
+
for entry in recent:
|
|
2791
|
+
print(f" [{entry.get('ts', '')}] {entry.get('action', '')} ({entry.get('actor', '')})")
|
|
2792
|
+
print(f"\n下一步:{current_stage_next_instruction(wf_state)}")
|
|
2793
|
+
|
|
2794
|
+
|
|
2795
|
+
# done 命令:清理临时回退副本,标记 Run 为 completed,写结束时间
|
|
2796
|
+
# 不再二次确认;保留正式产物;不改写 bug/索引.md
|
|
2797
|
+
def cmd_done(args) -> None:
|
|
2798
|
+
# 定位项目根
|
|
2799
|
+
project_root = resolve_project_root()
|
|
2800
|
+
if project_root is None:
|
|
2801
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
2802
|
+
sys.exit(1)
|
|
2803
|
+
refuse_if_pending_start_transaction(project_root)
|
|
2804
|
+
|
|
2805
|
+
# 读 state
|
|
2806
|
+
wf_state = state_mod.load_state(project_root)
|
|
2807
|
+
if wf_state is None:
|
|
2808
|
+
print("错误:还没启动工作流")
|
|
2809
|
+
sys.exit(1)
|
|
2810
|
+
# 已经是 completed
|
|
2811
|
+
if wf_state.run_status == "completed":
|
|
2812
|
+
print("错误:Run 已经是 completed 状态")
|
|
2813
|
+
sys.exit(1)
|
|
2814
|
+
# 已经是 aborted
|
|
2815
|
+
if wf_state.run_status == "aborted":
|
|
2816
|
+
print("错误:Run 已经是 aborted 状态")
|
|
2817
|
+
sys.exit(1)
|
|
2818
|
+
# 前置检查:current_stage 必须是 "completed"(末段 --confirmed 推进后)
|
|
2819
|
+
if wf_state.current_stage != "completed":
|
|
2820
|
+
print(f"错误:还有未完成的 stage(当前: {wf_state.current_stage}),"
|
|
2821
|
+
f"请先完成所有 stage 的 gate --confirmed")
|
|
2822
|
+
sys.exit(1)
|
|
2823
|
+
|
|
2824
|
+
try:
|
|
2825
|
+
cleaned_snapshots = rollback_mod.cleanup(project_root, wf_state.workflow_id)
|
|
2826
|
+
except (OSError, ValueError) as exc:
|
|
2827
|
+
print("═══ 工作流完成失败 ═══")
|
|
2828
|
+
print(f"详情: 无法清理临时回退副本:{exc}")
|
|
2829
|
+
print_next_step("保留当前 Run,处理回退目录权限后重新调 `workflow done`")
|
|
2830
|
+
return
|
|
2831
|
+
wf_state.rollback = state_mod.RollbackState()
|
|
2832
|
+
# 标记 Run 为 completed
|
|
2833
|
+
wf_state.run_status = "completed"
|
|
2834
|
+
# 写结束时间
|
|
2835
|
+
wf_state.ended_at = state_mod.now_iso()
|
|
2836
|
+
# 保存 state
|
|
2837
|
+
state_mod.save_state(project_root, wf_state)
|
|
2838
|
+
# 写 journal:Run 完成
|
|
2839
|
+
journal_mod.append_entry(project_root, "Run 完成", "workflow.py",
|
|
2840
|
+
workflow_id=wf_state.workflow_id,
|
|
2841
|
+
cleaned_rollback_paths=cleaned_snapshots)
|
|
2842
|
+
|
|
2843
|
+
# 打印完成信息
|
|
2844
|
+
print(f"═══ 工作流完成 ═══")
|
|
2845
|
+
print(f"workflow_id: {wf_state.workflow_id}")
|
|
2846
|
+
print(f"完成时间: {wf_state.ended_at}")
|
|
2847
|
+
# 下一步:工作流结束
|
|
2848
|
+
print_next_step("工作流完成。本次 workflow 结束。")
|
|
2849
|
+
|
|
2850
|
+
|
|
2851
|
+
# abort 命令:完整恢复本轮开工前受管内容,清理临时副本后才标记整轮已作废。
|
|
2852
|
+
def cmd_abort(args) -> None:
|
|
2853
|
+
project_root = resolve_project_root()
|
|
2854
|
+
if project_root is None:
|
|
2855
|
+
print("错误:找不到 .workflow_loop/ 目录。")
|
|
2856
|
+
sys.exit(1)
|
|
2857
|
+
refuse_if_pending_start_transaction(project_root)
|
|
2858
|
+
|
|
2859
|
+
wf_state = state_mod.load_state(project_root)
|
|
2860
|
+
if wf_state is None:
|
|
2861
|
+
print("错误:还没启动工作流")
|
|
2862
|
+
sys.exit(1)
|
|
2863
|
+
if wf_state.run_status != "active":
|
|
2864
|
+
print(
|
|
2865
|
+
f"错误:当前轮次状态为 {wf_state.run_status},"
|
|
2866
|
+
"只有 active(仍在进行)才能执行 abort(整轮作废)"
|
|
2867
|
+
)
|
|
2868
|
+
sys.exit(1)
|
|
2869
|
+
|
|
2870
|
+
restored_paths: list[str] = []
|
|
2871
|
+
if wf_state.rollback.restored_at is None:
|
|
2872
|
+
if wf_state.rollback.restore_started_at is None:
|
|
2873
|
+
ok, issues, abort_manifest = rollback_mod.preflight_abort(
|
|
2874
|
+
project_root,
|
|
2875
|
+
wf_state,
|
|
2876
|
+
)
|
|
2877
|
+
print("═══ 整轮作废恢复预检 ═══")
|
|
2878
|
+
if not ok or abort_manifest is None:
|
|
2879
|
+
print("预检失败,尚未修改任何项目内容:")
|
|
2880
|
+
for issue in issues or ["没有得到完整恢复清单"]:
|
|
2881
|
+
print(f" - {issue}")
|
|
2882
|
+
journal_mod.append_entry(
|
|
2883
|
+
project_root,
|
|
2884
|
+
"整轮作废预检失败",
|
|
2885
|
+
"workflow.py",
|
|
2886
|
+
workflow_id=wf_state.workflow_id,
|
|
2887
|
+
issues=issues,
|
|
2888
|
+
)
|
|
2889
|
+
print_next_step(
|
|
2890
|
+
"保留当前轮次为 active(仍在进行);先解决缺失或损坏的开工副本,"
|
|
2891
|
+
"再重新执行 `workflow abort`(整轮作废)"
|
|
2892
|
+
)
|
|
2893
|
+
return
|
|
2894
|
+
|
|
2895
|
+
print("以下是本次会恢复或删除的受管项目:")
|
|
2896
|
+
for item in abort_manifest.get("items", []):
|
|
2897
|
+
if item.get("kind") == "file":
|
|
2898
|
+
action = "恢复开工前内容" if item.get("original_exists") else "删除本轮新文件"
|
|
2899
|
+
print(f" - {item.get('path')}:{action}")
|
|
2900
|
+
elif item.get("kind") == "project_fields":
|
|
2901
|
+
print(" - .workflow_loop/project.json:恢复本轮受管项目字段")
|
|
2902
|
+
print("清单外文件不会读取、恢复或删除。")
|
|
2903
|
+
wf_state.rollback.restore_started_at = state_mod.now_iso()
|
|
2904
|
+
state_mod.save_state(project_root, wf_state)
|
|
2905
|
+
journal_mod.append_entry(
|
|
2906
|
+
project_root,
|
|
2907
|
+
"整轮作废恢复开始",
|
|
2908
|
+
"workflow.py",
|
|
2909
|
+
workflow_id=wf_state.workflow_id,
|
|
2910
|
+
item_ids=[
|
|
2911
|
+
item.get("id")
|
|
2912
|
+
for item in abort_manifest.get("items", [])
|
|
2913
|
+
],
|
|
2914
|
+
restore_started_at=wf_state.rollback.restore_started_at,
|
|
2915
|
+
)
|
|
2916
|
+
|
|
2917
|
+
restored_paths, failures = rollback_mod.restore_full_run(
|
|
2918
|
+
project_root,
|
|
2919
|
+
wf_state,
|
|
2920
|
+
)
|
|
2921
|
+
if failures:
|
|
2922
|
+
journal_mod.append_entry(
|
|
2923
|
+
project_root,
|
|
2924
|
+
"整轮作废恢复未完成",
|
|
2925
|
+
"workflow.py",
|
|
2926
|
+
workflow_id=wf_state.workflow_id,
|
|
2927
|
+
restored_items=restored_paths,
|
|
2928
|
+
failures=failures,
|
|
2929
|
+
)
|
|
2930
|
+
print("═══ 整轮作废恢复未完成 ═══")
|
|
2931
|
+
if restored_paths:
|
|
2932
|
+
print(f"本次已恢复: {restored_paths}")
|
|
2933
|
+
print("未完成项目:")
|
|
2934
|
+
for failure in failures:
|
|
2935
|
+
print(f" - {failure}")
|
|
2936
|
+
print_next_step(
|
|
2937
|
+
"回退副本和逐项进度已保留,当前轮次仍是 active(仍在进行);"
|
|
2938
|
+
"处理上面的具体问题后重新执行 `workflow abort`,已完成项目不会再次覆盖"
|
|
2939
|
+
)
|
|
2940
|
+
return
|
|
2941
|
+
|
|
2942
|
+
wf_state.rollback.restored_at = state_mod.now_iso()
|
|
2943
|
+
state_mod.save_state(project_root, wf_state)
|
|
2944
|
+
journal_mod.append_entry(
|
|
2945
|
+
project_root,
|
|
2946
|
+
"整轮作废项目已恢复",
|
|
2947
|
+
"workflow.py",
|
|
2948
|
+
workflow_id=wf_state.workflow_id,
|
|
2949
|
+
restored_items=restored_paths,
|
|
2950
|
+
restored_at=wf_state.rollback.restored_at,
|
|
2951
|
+
)
|
|
2952
|
+
|
|
2953
|
+
try:
|
|
2954
|
+
cleaned_snapshots = rollback_mod.cleanup(project_root, wf_state.workflow_id)
|
|
2955
|
+
except (OSError, ValueError) as exc:
|
|
2956
|
+
journal_mod.append_entry(
|
|
2957
|
+
project_root,
|
|
2958
|
+
"整轮作废临时副本清理失败",
|
|
2959
|
+
"workflow.py",
|
|
2960
|
+
workflow_id=wf_state.workflow_id,
|
|
2961
|
+
reason=str(exc),
|
|
2962
|
+
restored_items=restored_paths,
|
|
2963
|
+
)
|
|
2964
|
+
print("═══ 整轮作废清理未完成 ═══")
|
|
2965
|
+
print(f"详情: 项目内容已经恢复,但临时回退副本清理失败:{exc}")
|
|
2966
|
+
print_next_step(
|
|
2967
|
+
"当前轮次仍是 active(仍在进行);处理回退目录权限或路径问题后重新执行 "
|
|
2968
|
+
"`workflow abort`,重试只继续清理,不再恢复项目文件"
|
|
2969
|
+
)
|
|
2970
|
+
return
|
|
2971
|
+
|
|
2972
|
+
wf_state.rollback.cleanup_completed_at = state_mod.now_iso()
|
|
2973
|
+
# 临时清单已经删除,只清空失效的清单引用;保留三段恢复时间供状态审计。
|
|
2974
|
+
wf_state.rollback.manifest_path = None
|
|
2975
|
+
wf_state.rollback.manifest_hash = None
|
|
2976
|
+
wf_state.rollback.prepared_at = None
|
|
2977
|
+
wf_state.rollback.plan_hash = None
|
|
2978
|
+
wf_state.rollback.code_baseline_hash = None
|
|
2979
|
+
wf_state.rollback.planned_paths = []
|
|
2980
|
+
wf_state.run_status = "aborted"
|
|
2981
|
+
wf_state.aborted_at = state_mod.now_iso()
|
|
2982
|
+
state_mod.save_state(project_root, wf_state)
|
|
2983
|
+
journal_mod.append_entry(
|
|
2984
|
+
project_root,
|
|
2985
|
+
"整轮已作废",
|
|
2986
|
+
"workflow.py",
|
|
2987
|
+
workflow_id=wf_state.workflow_id,
|
|
2988
|
+
restored_items=restored_paths,
|
|
2989
|
+
cleaned_rollback_paths=cleaned_snapshots,
|
|
2990
|
+
restore_started_at=wf_state.rollback.restore_started_at,
|
|
2991
|
+
restored_at=wf_state.rollback.restored_at,
|
|
2992
|
+
cleanup_completed_at=wf_state.rollback.cleanup_completed_at,
|
|
2993
|
+
aborted_at=wf_state.aborted_at,
|
|
2994
|
+
)
|
|
2995
|
+
|
|
2996
|
+
print("═══ 工作流整轮已作废 ═══")
|
|
2997
|
+
print(f"workflow_id: {wf_state.workflow_id}")
|
|
2998
|
+
print(f"作废时间: {wf_state.aborted_at}")
|
|
2999
|
+
if restored_paths:
|
|
3000
|
+
print(f"本次调用恢复的项目: {restored_paths}")
|
|
3001
|
+
print("本轮正式产物副本和回退副本已经删除,只保留作废与恢复结果记录。")
|
|
3002
|
+
print_next_step(
|
|
3003
|
+
f"本轮已结束;有新需求时由 AI 执行 `workflow start --intent {wf_state.intent}` 开始新一轮"
|
|
3004
|
+
)
|
|
3005
|
+
|
|
3006
|
+
|
|
3007
|
+
# _install-project 内部命令:安装当前项目(只由官方安装脚本在确认后调用)
|
|
3008
|
+
# 不显示在普通帮助中;必须携带安装脚本生成的一次性事务文件
|
|
3009
|
+
# 项目根用 cwd(安装脚本已让用户确认目录)
|
|
3010
|
+
def cmd_internal_install_project(args) -> None:
|
|
3011
|
+
# 项目根 = 当前工作目录
|
|
3012
|
+
project_root = os.getcwd()
|
|
3013
|
+
# 调 installer 执行一次性事务安装:事务缺失、已使用、版本或路径不符都在写入前失败
|
|
3014
|
+
code = installer_mod.install_project_transaction(project_root, args.transaction)
|
|
3015
|
+
# 退出
|
|
3016
|
+
sys.exit(code)
|
|
3017
|
+
|
|
3018
|
+
|
|
3019
|
+
# Windows 下 stdout/stderr 被脚本捕获时可能退回本地西文编码,统一改成 UTF-8。
|
|
3020
|
+
def _configure_utf8_output() -> None:
|
|
3021
|
+
for stream_name in ("stdout", "stderr"):
|
|
3022
|
+
stream = getattr(sys, stream_name, None)
|
|
3023
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
3024
|
+
if not callable(reconfigure):
|
|
3025
|
+
continue
|
|
3026
|
+
try:
|
|
3027
|
+
reconfigure(encoding="utf-8")
|
|
3028
|
+
except (OSError, ValueError):
|
|
3029
|
+
pass
|
|
3030
|
+
|
|
3031
|
+
|
|
3032
|
+
# CLI 入口:解析参数、分发到对应 handler
|
|
3033
|
+
def main() -> None:
|
|
3034
|
+
_configure_utf8_output()
|
|
3035
|
+
# 创建 argparse 解析器
|
|
3036
|
+
parser = argparse.ArgumentParser(
|
|
3037
|
+
description="workflow_loop 工作流管理 CLI",
|
|
3038
|
+
prog="workflow",
|
|
3039
|
+
)
|
|
3040
|
+
# --version:固定产品身份查询,输出 "workflow-loop 0.1.0"
|
|
3041
|
+
# 安装脚本用它核对同名命令身份和兼容版本
|
|
3042
|
+
parser.add_argument("--version", action="version", version=PRODUCT_IDENTITY)
|
|
3043
|
+
# 子命令。metavar 固定列出公开命令,内部 _install-project 不出现在普通帮助中
|
|
3044
|
+
subparsers = parser.add_subparsers(
|
|
3045
|
+
dest="command",
|
|
3046
|
+
help="可用命令",
|
|
3047
|
+
metavar="{start,discuss,test,acceptance,gate,status,done,abort,return}",
|
|
3048
|
+
)
|
|
3049
|
+
|
|
3050
|
+
# start 命令
|
|
3051
|
+
start_parser = subparsers.add_parser("start", help="启动工作流或检查状态")
|
|
3052
|
+
# --intent:工作意图(不带时只读状态检查)
|
|
3053
|
+
start_parser.add_argument("--intent", choices=INTENT_CHOICES, default=None,
|
|
3054
|
+
help="工作意图(不带时只读状态检查)")
|
|
3055
|
+
# --confirm-clean:从零做清场确认(仅 from_scratch)
|
|
3056
|
+
start_parser.add_argument("--confirm-clean", action="store_true",
|
|
3057
|
+
help="从零做清场确认(仅 from_scratch)")
|
|
3058
|
+
|
|
3059
|
+
# discuss 命令(无参数,读 state.current_stage)
|
|
3060
|
+
subparsers.add_parser("discuss", help="加载当前 stage 提示词")
|
|
3061
|
+
|
|
3062
|
+
# test 命令:测试计划阶段登记项目入口;测试执行阶段登记并执行主题测试。
|
|
3063
|
+
test_parser = subparsers.add_parser("test", help="登记项目测试入口、登记或执行主题测试")
|
|
3064
|
+
test_subparsers = test_parser.add_subparsers(dest="test_action", required=True)
|
|
3065
|
+
test_entry_parser = test_subparsers.add_parser(
|
|
3066
|
+
"entry",
|
|
3067
|
+
help="在测试计划环节登记项目全量测试入口(按操作系统的参数数组;只登记不运行)",
|
|
3068
|
+
)
|
|
3069
|
+
test_entry_parser.add_argument("--default", nargs="+", help="默认入口参数数组")
|
|
3070
|
+
test_entry_parser.add_argument("--windows", nargs="+", help="Windows 入口参数数组")
|
|
3071
|
+
test_entry_parser.add_argument("--linux", nargs="+", help="Linux 入口参数数组")
|
|
3072
|
+
test_entry_parser.add_argument("--darwin", nargs="+", help="macOS 入口参数数组")
|
|
3073
|
+
test_entry_parser.add_argument(
|
|
3074
|
+
"--script",
|
|
3075
|
+
action="append",
|
|
3076
|
+
default=[],
|
|
3077
|
+
help="本轮新建或将修改的统一入口脚本路径;多个脚本时重复填写,必须在写脚本前登记",
|
|
3078
|
+
)
|
|
3079
|
+
test_prepare_parser = test_subparsers.add_parser("prepare", help="登记一个测试项的真实命令")
|
|
3080
|
+
test_prepare_parser.add_argument("--topic", required=True, help="验收主题名称")
|
|
3081
|
+
test_prepare_parser.add_argument("--tc", required=True, help="测试项编号,例如 TC-01")
|
|
3082
|
+
test_prepare_parser.add_argument(
|
|
3083
|
+
"--timeout",
|
|
3084
|
+
type=int,
|
|
3085
|
+
default=test_execution_mod.DEFAULT_TIMEOUT_SECONDS,
|
|
3086
|
+
help="单个测试项超时秒数,默认 600",
|
|
3087
|
+
)
|
|
3088
|
+
test_prepare_parser.add_argument(
|
|
3089
|
+
"--cwd",
|
|
3090
|
+
default=None,
|
|
3091
|
+
help="测试工作目录(项目内相对路径;默认项目根)",
|
|
3092
|
+
)
|
|
3093
|
+
test_prepare_parser.add_argument(
|
|
3094
|
+
"command_argv",
|
|
3095
|
+
nargs=argparse.REMAINDER,
|
|
3096
|
+
help="在 -- 后写实际测试命令及参数",
|
|
3097
|
+
)
|
|
3098
|
+
test_run_parser = test_subparsers.add_parser("run", help="执行尚无当前成功记录的已登记主题测试")
|
|
3099
|
+
test_run_parser.add_argument(
|
|
3100
|
+
"--parallel",
|
|
3101
|
+
type=int,
|
|
3102
|
+
default=None,
|
|
3103
|
+
help="最多并行执行的独立主题数;默认读取 project.json 的 test_parallelism",
|
|
3104
|
+
)
|
|
3105
|
+
|
|
3106
|
+
# acceptance 命令:用户在聊天中回答后,由 AI 记录当前 AC 的验收事实。
|
|
3107
|
+
acceptance_parser = subparsers.add_parser("acceptance", help="记录主题验收回答")
|
|
3108
|
+
acceptance_subparsers = acceptance_parser.add_subparsers(
|
|
3109
|
+
dest="acceptance_action",
|
|
3110
|
+
required=True,
|
|
3111
|
+
)
|
|
3112
|
+
acceptance_record_parser = acceptance_subparsers.add_parser(
|
|
3113
|
+
"record",
|
|
3114
|
+
help="记录一条人工或混合验收条件",
|
|
3115
|
+
)
|
|
3116
|
+
acceptance_record_parser.add_argument("--topic", required=True, help="验收主题名称")
|
|
3117
|
+
acceptance_record_parser.add_argument(
|
|
3118
|
+
"--criterion",
|
|
3119
|
+
required=True,
|
|
3120
|
+
help="验收条件编号,例如 AC-01",
|
|
3121
|
+
)
|
|
3122
|
+
acceptance_record_parser.add_argument(
|
|
3123
|
+
"--result",
|
|
3124
|
+
required=True,
|
|
3125
|
+
choices=("passed", "failed", "blocked"),
|
|
3126
|
+
help="验收结果:passed(通过)、failed(未通过)、blocked(无法继续验证)",
|
|
3127
|
+
)
|
|
3128
|
+
acceptance_record_parser.add_argument(
|
|
3129
|
+
"--actual-result",
|
|
3130
|
+
required=True,
|
|
3131
|
+
help="验收者实际观察到的结果",
|
|
3132
|
+
)
|
|
3133
|
+
acceptance_record_parser.add_argument(
|
|
3134
|
+
"--answer",
|
|
3135
|
+
required=True,
|
|
3136
|
+
help="验收者的实际回答",
|
|
3137
|
+
)
|
|
3138
|
+
acceptance_record_parser.add_argument(
|
|
3139
|
+
"--evidence",
|
|
3140
|
+
default="",
|
|
3141
|
+
help="可选证据说明;没有独立证据时可以省略",
|
|
3142
|
+
)
|
|
3143
|
+
|
|
3144
|
+
# gate 命令
|
|
3145
|
+
gate_parser = subparsers.add_parser(
|
|
3146
|
+
"gate",
|
|
3147
|
+
help="推进当前环节三道门:讨论完成、程序检查、用户确认",
|
|
3148
|
+
)
|
|
3149
|
+
# stage 名(位置参数)
|
|
3150
|
+
gate_parser.add_argument(
|
|
3151
|
+
"stage",
|
|
3152
|
+
help="当前环节的程序标识;workflow status 会同时显示它的中文含义",
|
|
3153
|
+
)
|
|
3154
|
+
# --discuss-done:第 1 道闸
|
|
3155
|
+
gate_parser.add_argument("--discuss-done", action="store_true",
|
|
3156
|
+
help="第一道门:记录当前问题已聊清楚,可以开始产出")
|
|
3157
|
+
# --confirmed:第 3 道闸
|
|
3158
|
+
gate_parser.add_argument("--confirmed", action="store_true",
|
|
3159
|
+
help="第三道门:记录用户看过当前结果并同意,随后进入下一环节")
|
|
3160
|
+
# --skip:跳过 stage(仅 spike)
|
|
3161
|
+
gate_parser.add_argument("--skip", action="store_true",
|
|
3162
|
+
help="跳过 stage(仅 spike)")
|
|
3163
|
+
# --rebaseline:用户确认当前代码作为 impl 的新实施前基线
|
|
3164
|
+
gate_parser.add_argument("--rebaseline", action="store_true",
|
|
3165
|
+
help="重设 impl 实施前代码基线(仅用户确认后使用)")
|
|
3166
|
+
gate_parser.add_argument(
|
|
3167
|
+
"--prepare-code",
|
|
3168
|
+
action="store_true",
|
|
3169
|
+
help="保存 impl 计划修改文件的真实修改前内容,供整个 Run 中止时回退",
|
|
3170
|
+
)
|
|
3171
|
+
# --accept-existing-code:用户确认代码在计划确认前已经是本次实施结果
|
|
3172
|
+
gate_parser.add_argument("--accept-existing-code", action="store_true",
|
|
3173
|
+
help="确认当前已有代码就是本次实施结果(仅 impl)")
|
|
3174
|
+
# --accept-existing-test-code:上游计划变化后确认现有测试代码仍然适用
|
|
3175
|
+
gate_parser.add_argument(
|
|
3176
|
+
"--accept-existing-test-code",
|
|
3177
|
+
action="store_true",
|
|
3178
|
+
help="确认当前已有测试代码仍覆盖最新测试计划(仅 test_code)",
|
|
3179
|
+
)
|
|
3180
|
+
# status 命令(旧状态可能先迁移阶段路径)
|
|
3181
|
+
subparsers.add_parser("status", help="打印状态摘要")
|
|
3182
|
+
# done 命令
|
|
3183
|
+
subparsers.add_parser("done", help="标记完成")
|
|
3184
|
+
# abort 命令
|
|
3185
|
+
subparsers.add_parser("abort", help="作废当前 Run")
|
|
3186
|
+
|
|
3187
|
+
# return 命令:测试失败或发现上游问题时,由用户确认后退回对应阶段。
|
|
3188
|
+
return_parser = subparsers.add_parser("return", help="退回当前阶段之前的指定阶段")
|
|
3189
|
+
return_parser.add_argument(
|
|
3190
|
+
"--to",
|
|
3191
|
+
required=True,
|
|
3192
|
+
help="退回目标阶段(必须是本轮实际路径中当前阶段之前的真实环节)",
|
|
3193
|
+
)
|
|
3194
|
+
return_parser.add_argument(
|
|
3195
|
+
"--topic",
|
|
3196
|
+
action="append",
|
|
3197
|
+
help="直接受影响主题;可重复填写。已有主题时必须明确填写或使用 --all-topics",
|
|
3198
|
+
)
|
|
3199
|
+
return_parser.add_argument(
|
|
3200
|
+
"--all-topics",
|
|
3201
|
+
action="store_true",
|
|
3202
|
+
help="明确标记当前全部主题都受影响",
|
|
3203
|
+
)
|
|
3204
|
+
return_parser.add_argument("--reason", required=True, help="退回原因")
|
|
3205
|
+
|
|
3206
|
+
# _install-project 内部命令(只由官方安装脚本调用;不出现在普通帮助的公开命令列表中)
|
|
3207
|
+
internal_install_parser = subparsers.add_parser("_install-project")
|
|
3208
|
+
internal_install_parser.add_argument(
|
|
3209
|
+
"--transaction",
|
|
3210
|
+
required=True,
|
|
3211
|
+
help="官方安装脚本生成的一次性安装事务文件路径",
|
|
3212
|
+
)
|
|
3213
|
+
|
|
3214
|
+
# 解析参数
|
|
3215
|
+
args = parser.parse_args()
|
|
3216
|
+
|
|
3217
|
+
# 没传命令 → 打印 help
|
|
3218
|
+
if args.command is None:
|
|
3219
|
+
parser.print_help()
|
|
3220
|
+
sys.exit(1)
|
|
3221
|
+
|
|
3222
|
+
# 分发到对应 handler
|
|
3223
|
+
if args.command == "start":
|
|
3224
|
+
cmd_start(args)
|
|
3225
|
+
elif args.command == "discuss":
|
|
3226
|
+
cmd_discuss(args)
|
|
3227
|
+
elif args.command == "test":
|
|
3228
|
+
if args.test_action == "entry":
|
|
3229
|
+
cmd_test_entry(args)
|
|
3230
|
+
elif args.test_action == "prepare":
|
|
3231
|
+
cmd_test_prepare(args)
|
|
3232
|
+
else:
|
|
3233
|
+
cmd_test_run(args)
|
|
3234
|
+
elif args.command == "acceptance":
|
|
3235
|
+
cmd_acceptance_record(args)
|
|
3236
|
+
elif args.command == "gate":
|
|
3237
|
+
cmd_gate(args)
|
|
3238
|
+
elif args.command == "status":
|
|
3239
|
+
cmd_status(args)
|
|
3240
|
+
elif args.command == "done":
|
|
3241
|
+
cmd_done(args)
|
|
3242
|
+
elif args.command == "abort":
|
|
3243
|
+
cmd_abort(args)
|
|
3244
|
+
elif args.command == "return":
|
|
3245
|
+
cmd_return(args)
|
|
3246
|
+
elif args.command == "_install-project":
|
|
3247
|
+
cmd_internal_install_project(args)
|
|
3248
|
+
else:
|
|
3249
|
+
# 未知命令(argparse 应该已经拦了,这是兜底)
|
|
3250
|
+
print(f"未知命令: {args.command}")
|
|
3251
|
+
parser.print_help()
|
|
3252
|
+
sys.exit(1)
|
|
3253
|
+
|
|
3254
|
+
|
|
3255
|
+
# 脚本直接运行时调 main
|
|
3256
|
+
if __name__ == "__main__":
|
|
3257
|
+
main()
|