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.
Files changed (60) hide show
  1. workflow_loop/__init__.py +6 -0
  2. workflow_loop/acceptance_records.py +338 -0
  3. workflow_loop/artifact_paths.py +278 -0
  4. workflow_loop/artifact_validation.py +1738 -0
  5. workflow_loop/bug_record.py +203 -0
  6. workflow_loop/cli.py +3257 -0
  7. workflow_loop/data/Standardized_Repository/acceptance/acceptance.md +119 -0
  8. workflow_loop/data/Standardized_Repository/acceptance/acceptance_plan.md +105 -0
  9. workflow_loop/data/Standardized_Repository/code_design/code_design.md +204 -0
  10. workflow_loop/data/Standardized_Repository/code_design/project_design_init.md +152 -0
  11. workflow_loop/data/Standardized_Repository/code_design/revise_code_design.md +32 -0
  12. workflow_loop/data/Standardized_Repository/code_design/update_code_design.md +94 -0
  13. workflow_loop/data/Standardized_Repository/global/document_writing.md +77 -0
  14. workflow_loop/data/Standardized_Repository/global/workflow_lifecycle.md +91 -0
  15. workflow_loop/data/Standardized_Repository/impl/code_implementation.md +85 -0
  16. workflow_loop/data/Standardized_Repository/impl/impl.md +164 -0
  17. workflow_loop/data/Standardized_Repository/qa/test.md +167 -0
  18. workflow_loop/data/Standardized_Repository/qa/test_code.md +121 -0
  19. workflow_loop/data/Standardized_Repository/qa/test_code_implementation.md +67 -0
  20. workflow_loop/data/Standardized_Repository/qa/test_plan.md +160 -0
  21. workflow_loop/data/Standardized_Repository/reproduce/reproduce.md +60 -0
  22. workflow_loop/data/Standardized_Repository/spec/spec.md +138 -0
  23. workflow_loop/data/Standardized_Repository/spike/spike.md +236 -0
  24. workflow_loop/data/Template_Repository/acceptance/acceptance_plan.md +142 -0
  25. workflow_loop/data/Template_Repository/acceptance/acceptance_result.md +108 -0
  26. workflow_loop/data/Template_Repository/code_design/code_design.md +260 -0
  27. workflow_loop/data/Template_Repository/code_design/project_design_init_evidence.md +39 -0
  28. workflow_loop/data/Template_Repository/impl/impl.md +112 -0
  29. workflow_loop/data/Template_Repository/qa/test.md +102 -0
  30. workflow_loop/data/Template_Repository/qa/test_plan.md +100 -0
  31. workflow_loop/data/Template_Repository/reproduce/reproduce.md +82 -0
  32. workflow_loop/data/Template_Repository/spec/spec.md +222 -0
  33. workflow_loop/data/Template_Repository/spike/spike.md +135 -0
  34. workflow_loop/installer.py +632 -0
  35. workflow_loop/journal.py +78 -0
  36. workflow_loop/path_composer.py +152 -0
  37. workflow_loop/process_runner.py +176 -0
  38. workflow_loop/project.py +397 -0
  39. workflow_loop/role_doc.py +133 -0
  40. workflow_loop/rollback.py +1738 -0
  41. workflow_loop/spike_validation.py +379 -0
  42. workflow_loop/stage_materials.py +169 -0
  43. workflow_loop/stages/__init__.py +45 -0
  44. workflow_loop/stages/base.py +164 -0
  45. workflow_loop/stages/stages.py +1191 -0
  46. workflow_loop/state.py +582 -0
  47. workflow_loop/test_entry.py +123 -0
  48. workflow_loop/test_execution.py +619 -0
  49. workflow_loop/test_mapping.py +568 -0
  50. workflow_loop/test_runner.py +134 -0
  51. workflow_loop/topic.py +114 -0
  52. workflow_loop/topic_relations.py +202 -0
  53. workflow_loop/traceability.py +533 -0
  54. workflow_loop/verification.py +971 -0
  55. workflow_loop-0.1.0.dist-info/METADATA +187 -0
  56. workflow_loop-0.1.0.dist-info/RECORD +60 -0
  57. workflow_loop-0.1.0.dist-info/WHEEL +5 -0
  58. workflow_loop-0.1.0.dist-info/entry_points.txt +2 -0
  59. workflow_loop-0.1.0.dist-info/licenses/LICENSE +21 -0
  60. workflow_loop-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1191 @@
1
+ import os
2
+ import re
3
+
4
+ from ..artifact_validation import (
5
+ PROJECT_INIT_EVIDENCE_PATH,
6
+ changed_stage_paths,
7
+ validate_acceptance_plan_documents,
8
+ validate_downstream_traceability,
9
+ validate_final_code_design_document,
10
+ validate_final_regression_state,
11
+ validate_inherited_topic_index,
12
+ validate_overall_acceptance_prerequisites,
13
+ validate_project_design_init_evidence,
14
+ validate_reproduce_documents,
15
+ validate_test_plan_documents,
16
+ validate_test_execution_results,
17
+ validate_topic_acceptance_results,
18
+ validate_topic_execution_results,
19
+ )
20
+ from .. import artifact_paths as artifact_paths_mod
21
+ from ..spike_validation import validate_spike_stage
22
+ from .. import test_execution as test_execution_mod
23
+ from .. import test_runner as test_runner_mod
24
+ from .. import test_entry as test_entry_mod
25
+ from ..project import load_project
26
+ from ..state import load_state
27
+ from ..test_mapping import (
28
+ automated_test_items,
29
+ automated_topics,
30
+ validate_workflow_test_markers,
31
+ )
32
+ from ..topic import (
33
+ candidate_topics,
34
+ current_workflow_topics,
35
+ missing_topic_documents,
36
+ topic_paths,
37
+ )
38
+ from ..verification import (
39
+ compute_code_snapshot_hash,
40
+ compute_non_test_code_snapshot_hash,
41
+ compute_test_code_snapshot_hash,
42
+ get_linked_product_design_paths,
43
+ )
44
+ from .. import rollback as rollback_mod
45
+ from .base import StageStrategy, clean_spike_tmp
46
+
47
+
48
+ # 产品设计 stage:工作流第一个环节,产出 spec/产品总说明.md + spec/功能_*.md
49
+ # 把用户需求拆成产品说明书 + 功能清单,后续 stage 都基于这里定的功能
50
+ class SpecStage(StageStrategy):
51
+ def name(self) -> str:
52
+ return "spec"
53
+
54
+ def artifact_paths(self) -> list[str]:
55
+ return [artifact_paths_mod.PRODUCT_OVERVIEW_DOC]
56
+
57
+ def role_doc_path(self) -> str | None:
58
+ return None
59
+
60
+ def prompt_doc_path(self) -> str | None:
61
+ return "Template_Repository/spec/spec.md"
62
+
63
+ def standard_doc_path(self) -> str | None:
64
+ return "Standardized_Repository/spec/spec.md"
65
+
66
+ # 门禁的代码侧校验(第 2 道闸)
67
+ # 检查产品总说明存在 + 当前链接的功能文档存在且属于本阶段修改
68
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
69
+ overview_rel = artifact_paths_mod.PRODUCT_OVERVIEW_DOC
70
+ overview_path = os.path.join(project_root, overview_rel)
71
+ if not os.path.exists(overview_path):
72
+ return (False, f"{overview_rel} 不存在")
73
+ # 当前有效产品功能只以产品总说明仍然链接的功能文档为准
74
+ linked_paths = [
75
+ path
76
+ for path in get_linked_product_design_paths(project_root)
77
+ if path != overview_rel
78
+ ]
79
+ if not linked_paths:
80
+ return (False, f"{overview_rel} 没有链接任何 功能_*.md 功能文档")
81
+ missing_features = [
82
+ path
83
+ for path in linked_paths
84
+ if not os.path.isfile(os.path.join(project_root, path))
85
+ ]
86
+ if missing_features:
87
+ return (False, f"产品总说明链接的功能文档不存在: {missing_features}")
88
+ # 功能文档标题保留完整显示名称
89
+ for path in linked_paths:
90
+ with open(os.path.join(project_root, path), "r", encoding="utf-8") as stream:
91
+ first_line = stream.readline().strip()
92
+ if not first_line.startswith("# 【功能】") or len(first_line) <= len("# 【功能】"):
93
+ return (False, f"{path} 的一级标题必须是“# 【功能】<功能名称>”")
94
+
95
+ changed_ok, changed_detail, changed_paths = changed_stage_paths(
96
+ project_root,
97
+ self.name(),
98
+ self.change_tracked_paths(project_root),
99
+ )
100
+ if not changed_ok:
101
+ return (False, changed_detail)
102
+
103
+ state = load_state(project_root)
104
+ if state is None:
105
+ return (False, "找不到当前工作流状态")
106
+ normalized_changed = [path.replace(os.sep, "/") for path in changed_paths]
107
+ product_changed = overview_rel in normalized_changed
108
+ feature_changed = any(
109
+ path.startswith("spec/功能_") for path in normalized_changed
110
+ )
111
+ if state.intent == "from_scratch" and (not product_changed or not feature_changed):
112
+ return (False, "从零创建产品时,产品总说明和至少一份功能文档都必须在本阶段新建")
113
+ if state.intent == "product_change" and not product_changed:
114
+ return (
115
+ False,
116
+ "修改产品时,产品总说明必须更新并记录本轮变化",
117
+ )
118
+ return (
119
+ True,
120
+ f"产品设计文档存在并且属于本阶段修改: 产品总说明 + {[os.path.basename(p) for p in linked_paths]}",
121
+ )
122
+
123
+ def change_tracked_paths(self, project_root: str) -> list[str]:
124
+ return get_linked_product_design_paths(project_root)
125
+
126
+ def instruction(self) -> str:
127
+ return (
128
+ "产品设计阶段:产出 spec/产品总说明.md(产品设计说明书 + 功能清单)"
129
+ "和 spec/功能_<功能文件标识>.md(功能拆分);文档标题写完整功能名称"
130
+ )
131
+
132
+
133
+ # 初步代码架构 stage:从已确认产品设计推导代码设计
134
+ class CodeDesignStage(StageStrategy):
135
+ def name(self) -> str:
136
+ return "code_design"
137
+
138
+ def artifact_paths(self) -> list[str]:
139
+ return [artifact_paths_mod.CODE_DESIGN_DOC]
140
+
141
+ def role_doc_path(self) -> str | None:
142
+ return None
143
+
144
+ def prompt_doc_path(self) -> str | None:
145
+ return "Template_Repository/code_design/code_design.md"
146
+
147
+ def standard_doc_path(self) -> str | None:
148
+ return "Standardized_Repository/code_design/code_design.md"
149
+
150
+ def instruction(self) -> str:
151
+ return (
152
+ "初步代码架构阶段:从已确认产品设计推导代码分层、关键节点和功能代码过程,"
153
+ f"产出 {artifact_paths_mod.CODE_DESIGN_DOC}"
154
+ )
155
+
156
+
157
+ # 穿刺 stage:识别并验证真实场景中的技术不确定性
158
+ class SpikeStage(StageStrategy):
159
+ def name(self) -> str:
160
+ return "spike"
161
+
162
+ def artifact_paths(self) -> list[str]:
163
+ return [artifact_paths_mod.SPIKE_INDEX_DOC]
164
+
165
+ def role_doc_path(self) -> str | None:
166
+ return None
167
+
168
+ def prompt_doc_path(self) -> str | None:
169
+ return "Template_Repository/spike/spike.md"
170
+
171
+ def standard_doc_path(self) -> str | None:
172
+ return "Standardized_Repository/spike/spike.md"
173
+
174
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
175
+ return validate_spike_stage(project_root)
176
+
177
+ def on_advance(self, project_root: str) -> list[str]:
178
+ return clean_spike_tmp(project_root)
179
+
180
+ def instruction(self) -> str:
181
+ return (
182
+ "穿刺阶段:先查产品设计、代码设计、相关代码和运行事实,识别真实场景中的技术不确定性;"
183
+ f"用户决定执行清单或全部跳过。正常执行时写 {artifact_paths_mod.SPIKE_INDEX_DOC} 和每项结论文档,"
184
+ "需要临时代码时放入 .workflow_loop/spike_tmp/,并在进入验收计划前同步受影响的设计文档。"
185
+ )
186
+
187
+
188
+ # 验收计划 stage:制定什么算完成的验收计划
189
+ class AcceptancePlanStage(StageStrategy):
190
+ def name(self) -> str:
191
+ return "acceptance_plan"
192
+
193
+ def artifact_paths(self) -> list[str]:
194
+ return [
195
+ artifact_paths_mod.TRACEABILITY_DOC,
196
+ artifact_paths_mod.ACCEPTANCE_INDEX_DOC,
197
+ "acceptance/",
198
+ ]
199
+
200
+ def role_doc_path(self) -> str | None:
201
+ return None
202
+
203
+ def prompt_doc_path(self) -> str | None:
204
+ return "Template_Repository/acceptance/acceptance_plan.md"
205
+
206
+ def standard_doc_path(self) -> str | None:
207
+ return "Standardized_Repository/acceptance/acceptance_plan.md"
208
+
209
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
210
+ acc_dir = os.path.join(project_root, "acceptance")
211
+ if not os.path.exists(acc_dir):
212
+ return (False, "acceptance/ 目录不存在")
213
+ state = load_state(project_root)
214
+ if state is None:
215
+ return (False, "找不到当前工作流状态")
216
+
217
+ if state.intent == "bugfix":
218
+ topics = current_workflow_topics(project_root)
219
+ if not topics:
220
+ return (False, "修 bug 的验收主题必须先在缺陷复现阶段确定")
221
+ else:
222
+ # 当前 Run 已记录主题时复核这些主题;否则从历史中排除旧主题,找本次新增主题。
223
+ topics = current_workflow_topics(project_root) or candidate_topics(project_root)
224
+ if not topics:
225
+ return (False, "没有找到本次新增的验收主题;主题名称不能复用项目历史中的名称")
226
+ missing = missing_topic_documents(project_root, "acceptance_plan", topics)
227
+ if missing:
228
+ return (False, f"缺少验收计划文档: {missing}")
229
+ return validate_acceptance_plan_documents(
230
+ project_root,
231
+ state.workflow_id,
232
+ topics,
233
+ )
234
+
235
+ def instruction(self) -> str:
236
+ return (
237
+ "验收计划阶段:从零开发和修改产品先确定本次需求的全部验收主题;"
238
+ "修 bug 复用缺陷复现阶段已经确认的主题。为每个主题写清什么算完成,"
239
+ f"确认主题关系,产出 {artifact_paths_mod.TRACEABILITY_DOC}、"
240
+ f"{artifact_paths_mod.ACCEPTANCE_INDEX_DOC} 和 acceptance/<主题文件标识>_验收计划.md。"
241
+ )
242
+
243
+
244
+ # 测试计划 stage:对验收条件做测试覆盖审查,登记项目全量入口
245
+ class TestPlanStage(StageStrategy):
246
+ def name(self) -> str:
247
+ return "test_plan"
248
+
249
+ def artifact_paths(self) -> list[str]:
250
+ return [artifact_paths_mod.QA_INDEX_DOC]
251
+
252
+ def role_doc_path(self) -> str | None:
253
+ return None
254
+
255
+ def prompt_doc_path(self) -> str | None:
256
+ return "Template_Repository/qa/test_plan.md"
257
+
258
+ def standard_doc_path(self) -> str | None:
259
+ return "Standardized_Repository/qa/test_plan.md"
260
+
261
+ # 门禁的代码侧校验(第 2 道闸):结构、环境和入口检查,不执行任何测试
262
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
263
+ qa_dir = os.path.join(project_root, "qa")
264
+ if not os.path.exists(qa_dir):
265
+ return (False, "qa/ 目录不存在")
266
+ index_md = os.path.join(project_root, artifact_paths_mod.QA_INDEX_DOC)
267
+ if not os.path.isfile(index_md):
268
+ return (False, f"{artifact_paths_mod.QA_INDEX_DOC} 不存在")
269
+ topics = current_workflow_topics(project_root)
270
+ if not topics:
271
+ return (False, "当前工作流还没有确认验收主题,请先完成 acceptance_plan")
272
+ missing = missing_topic_documents(project_root, "test_plan", topics)
273
+ if missing:
274
+ return (False, f"缺少测试计划文档: {missing}")
275
+ state = load_state(project_root)
276
+ if state is None:
277
+ return (False, "找不到当前工作流状态")
278
+ trace_ok, trace_detail = validate_downstream_traceability(
279
+ project_root,
280
+ state.workflow_id,
281
+ topics,
282
+ )
283
+ if not trace_ok:
284
+ return (False, trace_detail)
285
+ plan_ok, plan_detail = validate_test_plan_documents(
286
+ project_root,
287
+ state.workflow_id,
288
+ topics,
289
+ )
290
+ if not plan_ok:
291
+ return (False, plan_detail)
292
+ # 当前操作系统必须有可用的项目全量入口参数数组;只检查配置,不执行入口
293
+ argv, entry_detail = test_runner_mod.resolve_regression_entry(project_root)
294
+ if argv is None:
295
+ return (False, f"项目全量测试入口未就绪:{entry_detail}")
296
+ # 声明的入口脚本必须真实存在
297
+ for part in argv:
298
+ if ("/" in part or "\\" in part) and not part.startswith("-"):
299
+ script_path = os.path.join(project_root, part)
300
+ if not os.path.isfile(script_path):
301
+ return (False, f"项目全量测试入口脚本不存在: {part}")
302
+ project = load_project(project_root)
303
+ configured_scripts = test_entry_mod.referenced_project_scripts(
304
+ project.test_entry if project is not None and isinstance(project.test_entry, dict) else {}
305
+ )
306
+ if configured_scripts:
307
+ start_manifest = rollback_mod.read_start_baseline(
308
+ project_root,
309
+ state.workflow_id,
310
+ )
311
+ entries = start_manifest.get("entries", {}) if isinstance(start_manifest, dict) else {}
312
+ missing_backups = [
313
+ script for script in configured_scripts if script not in entries
314
+ ]
315
+ if missing_backups:
316
+ return (
317
+ False,
318
+ "项目全量测试入口脚本没有在修改前登记回退依据: "
319
+ f"{missing_backups}",
320
+ )
321
+ return (True, f"{plan_detail};当前平台全量入口: {argv}(本阶段不执行)")
322
+
323
+ def instruction(self) -> str:
324
+ return (
325
+ "测试计划阶段:对已确认验收条件做测试覆盖审查,"
326
+ "调查项目真实测试工具链并用 workflow test entry 登记按操作系统的全量入口参数数组;"
327
+ f"产出 qa/<主题文件标识>_测试计划.md + {artifact_paths_mod.QA_INDEX_DOC};"
328
+ "本阶段不执行修改前全量测试"
329
+ )
330
+
331
+
332
+ # 实施 stage:按验收主题承接实施前计划和真实代码实施。
333
+ class ImplStage(StageStrategy):
334
+ def name(self) -> str:
335
+ return "impl"
336
+
337
+ def artifact_paths(self) -> list[str]:
338
+ return [artifact_paths_mod.IMPL_INDEX_DOC]
339
+
340
+ def role_doc_path(self) -> str | None:
341
+ return None
342
+
343
+ def prompt_doc_path(self) -> str | None:
344
+ return "Template_Repository/impl/impl.md"
345
+
346
+ def standard_doc_path(self) -> str | None:
347
+ return "Standardized_Repository/impl/impl.md"
348
+
349
+ def additional_standard_doc_paths(self) -> list[str]:
350
+ return ["Standardized_Repository/impl/code_implementation.md"]
351
+
352
+ def _validate_topic_documents(
353
+ self,
354
+ project_root: str,
355
+ workflow_state,
356
+ *,
357
+ require_execution_record: bool,
358
+ ) -> tuple[bool, str, list[str]]:
359
+ topics = current_workflow_topics(project_root)
360
+ if not topics:
361
+ return (False, "当前工作流还没有确认验收主题,请先完成 acceptance_plan", [])
362
+
363
+ qa_ok, qa_detail = validate_inherited_topic_index(
364
+ project_root,
365
+ workflow_state.workflow_id,
366
+ topics,
367
+ artifact_paths_mod.QA_INDEX_DOC,
368
+ ["展示顺序", "验收主题", "前置主题", "验收计划", "测试计划", "测试结果"],
369
+ {
370
+ "验收计划": "../acceptance/{key}_验收计划.md",
371
+ "测试计划": "./{key}_测试计划.md",
372
+ "测试结果": "./{key}_测试结果.md",
373
+ },
374
+ {"测试结果": {"无自动化测试项"}},
375
+ )
376
+ if not qa_ok:
377
+ return (False, qa_detail, topics)
378
+
379
+ index_path = os.path.join(project_root, artifact_paths_mod.IMPL_INDEX_DOC)
380
+ if not os.path.isfile(index_path):
381
+ return (False, f"{artifact_paths_mod.IMPL_INDEX_DOC} 不存在", topics)
382
+ index_ok, index_detail = validate_inherited_topic_index(
383
+ project_root,
384
+ workflow_state.workflow_id,
385
+ topics,
386
+ artifact_paths_mod.IMPL_INDEX_DOC,
387
+ ["展示顺序", "验收主题", "前置主题", "验收计划", "测试计划", "实施文档"],
388
+ {
389
+ "验收计划": "../acceptance/{key}_验收计划.md",
390
+ "测试计划": "../qa/{key}_测试计划.md",
391
+ "实施文档": "./{key}_实施记录.md",
392
+ },
393
+ )
394
+ if not index_ok:
395
+ return (False, index_detail, topics)
396
+
397
+ missing_topics = []
398
+ for topic in topics:
399
+ topic_rel = topic_paths(project_root, topic)["impl_doc"]
400
+ topic_path = os.path.join(project_root, topic_rel)
401
+ if not os.path.isfile(topic_path):
402
+ missing_topics.append(topic_rel)
403
+ continue
404
+ with open(topic_path, "r", encoding="utf-8") as stream:
405
+ topic_content = stream.read()
406
+ if f"- 工作流编号:{workflow_state.workflow_id}" not in topic_content:
407
+ return (False, f"{topic_rel} 的工作流编号与当前工作流不一致", topics)
408
+ if f"- 验收主题:{topic}" not in topic_content:
409
+ return (False, f"{topic_rel} 的验收主题显示名称必须是“{topic}”", topics)
410
+ if "## 1. 实施依据" not in topic_content:
411
+ return (False, f"{topic_rel} 缺少“实施依据”", topics)
412
+ if "## 2. 实施前计划" not in topic_content:
413
+ return (False, f"{topic_rel} 缺少“实施前计划”", topics)
414
+ if "## 4. 计划与实际的差异" in topic_content:
415
+ return (False, f"{topic_rel} 不得包含“计划与实际的差异”章节", topics)
416
+ if "## 4. 上下游文档" not in topic_content:
417
+ return (False, f"{topic_rel} 缺少“上下游文档”", topics)
418
+ unresolved = self._subsection_text(topic_content, "2.4 未决问题")
419
+ if unresolved is None:
420
+ return (False, f"{topic_rel} 缺少“2.4 未决问题”", topics)
421
+ if self._has_unresolved_content(unresolved):
422
+ return (False, f"{topic_rel} 仍有未决问题,不能进入代码实施", topics)
423
+ if require_execution_record:
424
+ if "## 3. 实施后记录" not in topic_content:
425
+ return (False, f"{topic_rel} 缺少“实施后记录”", topics)
426
+ if "### 3.3 未完成内容" not in topic_content and "## 3.3 未完成内容" not in topic_content:
427
+ return (False, f"{topic_rel} 缺少“3.3 未完成内容”", topics)
428
+ unfinished = self._subsection_text(topic_content, "3.3 未完成内容")
429
+ if unfinished is None or self._has_unresolved_content(unfinished):
430
+ return (False, f"{topic_rel} 仍有未完成内容,不能通过实施门禁", topics)
431
+ if "测试结果:通过" in topic_content or "测试结果:失败" in topic_content:
432
+ return (False, f"{topic_rel} 不能提前填写正式测试结果", topics)
433
+
434
+ if missing_topics:
435
+ return (False, f"缺少主题实施文档: {missing_topics}", topics)
436
+ return (True, "", topics)
437
+
438
+ @staticmethod
439
+ def _subsection_text(content: str, heading: str) -> str | None:
440
+ match = re.search(
441
+ rf"^###\s+{re.escape(heading)}\s*$\n(.*?)(?=^###\s+|^##\s+|\Z)",
442
+ content,
443
+ re.MULTILINE | re.DOTALL,
444
+ )
445
+ return match.group(1).strip() if match else None
446
+
447
+ @staticmethod
448
+ def _has_unresolved_content(content: str) -> bool:
449
+ normalized = re.sub(r"[\s`*_-]", "", content)
450
+ return normalized not in {"暂无", "无"}
451
+
452
+ def validate_implementation_records(
453
+ self,
454
+ project_root: str,
455
+ workflow_state,
456
+ ) -> tuple[bool, str, list[str]]:
457
+ """只校验实施文档和追踪关系,不判断代码是否变化。"""
458
+ valid, detail, topics = self._validate_topic_documents(
459
+ project_root,
460
+ workflow_state,
461
+ require_execution_record=True,
462
+ )
463
+ if not valid:
464
+ return (False, detail, topics)
465
+ trace_ok, trace_detail = validate_downstream_traceability(
466
+ project_root,
467
+ workflow_state.workflow_id,
468
+ topics,
469
+ )
470
+ if not trace_ok:
471
+ return (False, trace_detail, topics)
472
+ return (True, "实施文档和需求交付追踪关系完整", topics)
473
+
474
+ def discussion_validate(self, project_root: str, workflow_state) -> tuple[bool, str]:
475
+ # 第一道门确认的是全部实施前计划。
476
+ valid, detail, _ = self._validate_topic_documents(
477
+ project_root,
478
+ workflow_state,
479
+ require_execution_record=False,
480
+ )
481
+ if not valid:
482
+ return (False, detail)
483
+
484
+ stage_state = workflow_state.stages.get(self.name())
485
+ if stage_state is None or stage_state.code_baseline_hash is None:
486
+ return (False, "缺少进入 impl 时的代码基线,不能确认计划前没有修改代码")
487
+ current_hash = compute_non_test_code_snapshot_hash(project_root)
488
+ if current_hash == stage_state.code_baseline_hash:
489
+ return (True, "全部验收主题的实施前计划已就绪,代码尚未修改")
490
+ # 实施中重新确认计划:已经保存首次原内容且全部实际变化都在计划内时,
491
+ # 允许在代码已变化的情况下重新通过讨论门(不覆盖首次副本)。
492
+ prepared_ok, prepared_detail, manifest = rollback_mod.validate_prepared(
493
+ project_root,
494
+ workflow_state,
495
+ require_current_plan=False,
496
+ )
497
+ if prepared_ok and manifest is not None:
498
+ try:
499
+ changed = rollback_mod.changed_paths_since_prepare(project_root, manifest)
500
+ except ValueError as exc:
501
+ return (False, str(exc))
502
+ planned = set()
503
+ try:
504
+ planned = set(rollback_mod.planned_code_paths(project_root, workflow_state.topics))
505
+ except ValueError as exc:
506
+ return (False, str(exc))
507
+ unexpected = sorted(set(changed) - planned - set(manifest.get("entries", {})))
508
+ if unexpected:
509
+ return (
510
+ False,
511
+ f"实施计划确认前存在计划外的代码变化,不能通过 impl 的第一道门: {unexpected}",
512
+ )
513
+ return (
514
+ True,
515
+ "实施计划重新确认:首次原内容已保存,当前变化均在计划内",
516
+ )
517
+ return (False, "实施计划确认前代码已经变化,不能通过 impl 的第一道门")
518
+
519
+ # 门禁的代码侧校验(第 2 道闸)
520
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
521
+ state = load_state(project_root)
522
+ if state is None:
523
+ return (False, "找不到当前工作流状态")
524
+ valid, detail, topics = self.validate_implementation_records(project_root, state)
525
+ if not valid:
526
+ return (False, detail)
527
+
528
+ stage_state = state.stages.get(self.name())
529
+ if stage_state is None or stage_state.code_baseline_hash is None:
530
+ return (False, "缺少进入 impl 时的代码基线,不能确认实施代码变化")
531
+ rollback_ok, rollback_detail, _ = rollback_mod.validate_prepared(
532
+ project_root,
533
+ state,
534
+ )
535
+ if not rollback_ok:
536
+ return (False, rollback_detail)
537
+ current_hash = compute_non_test_code_snapshot_hash(project_root)
538
+ if stage_state.existing_code_accepted_hash is not None:
539
+ if current_hash != stage_state.existing_code_accepted_hash:
540
+ return (False, "用户确认既有代码后代码又发生变化,不能通过实施门禁")
541
+ return (True, f"{len(topics)} 个验收主题的实施计划和实施记录完整,既有代码未发生变化")
542
+ if current_hash == stage_state.code_baseline_hash:
543
+ recovery = getattr(state, "recovery", None)
544
+ if (
545
+ recovery is not None
546
+ and recovery.source_stage
547
+ and "impl" in recovery.affected_stages
548
+ ):
549
+ return (
550
+ False,
551
+ "当前代码相对恢复基线没有变化;如果现有代码已经是本次实施结果,"
552
+ "请先调 workflow gate impl --accept-existing-code 明确确认,"
553
+ "否则修改代码后再调 workflow gate impl",
554
+ )
555
+ return (False, "实施代码没有相对计划确认时的代码基线发生变化")
556
+
557
+ changes_ok, changes_detail = rollback_mod.validate_implementation_changes(
558
+ project_root,
559
+ state,
560
+ )
561
+ if not changes_ok:
562
+ return (False, changes_detail)
563
+
564
+ trace_ok, trace_detail = validate_downstream_traceability(
565
+ project_root,
566
+ state.workflow_id,
567
+ topics,
568
+ )
569
+ if not trace_ok:
570
+ return (False, trace_detail)
571
+ return (
572
+ True,
573
+ f"{len(topics)} 个验收主题的实施计划和实施记录完整;{changes_detail}",
574
+ )
575
+
576
+ def instruction(self) -> str:
577
+ return (
578
+ "实施阶段:依据全部验收主题的验收计划和测试计划,先写完实施前计划;"
579
+ "用户确认后再修改真实代码,并在同一份 impl/<主题文件标识>_实施记录.md 中追加实施后记录"
580
+ )
581
+
582
+
583
+ def _test_code_paths(project_root: str) -> list[str]:
584
+ """找出项目中现有的测试代码路径,供 test_code 阶段建立变更基线。"""
585
+ code_suffixes = {".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs", ".java", ".kt", ".swift", ".ets"}
586
+ paths: list[str] = []
587
+ for root, dirs, files in os.walk(project_root):
588
+ dirs[:] = [
589
+ directory
590
+ for directory in dirs
591
+ if directory not in {".git", ".workflow_loop", "__pycache__", ".venv", "node_modules"}
592
+ ]
593
+ for filename in files:
594
+ relative_path = os.path.relpath(os.path.join(root, filename), project_root)
595
+ parts = relative_path.replace(os.sep, "/").split("/")
596
+ lowered = filename.lower()
597
+ if os.path.splitext(filename)[1].lower() not in code_suffixes:
598
+ continue
599
+ if (
600
+ "tests" in parts
601
+ or "test" in parts
602
+ or lowered.startswith("test_")
603
+ or lowered.endswith(("_test.py", ".test.js", ".test.ts", ".spec.js", ".spec.ts"))
604
+ ):
605
+ paths.append(relative_path)
606
+ return sorted(paths)
607
+
608
+
609
+ # 测试代码编写阶段:调查真实代码并写测试代码。
610
+ # 可以运行与当前修改直接相关的局部测试作为开发反馈,但不产出正式测试结果。
611
+ class TestCodeStage(StageStrategy):
612
+ def name(self) -> str:
613
+ return "test_code"
614
+
615
+ def artifact_paths(self) -> list[str]:
616
+ # 不同语言和框架的测试目录不同;具体测试文件由真实代码调查决定。
617
+ return []
618
+
619
+ def role_doc_path(self) -> str | None:
620
+ return None
621
+
622
+ def prompt_doc_path(self) -> str | None:
623
+ return None
624
+
625
+ def standard_doc_path(self) -> str | None:
626
+ return "Standardized_Repository/qa/test_code.md"
627
+
628
+ def additional_standard_doc_paths(self) -> list[str]:
629
+ return ["Standardized_Repository/qa/test_code_implementation.md"]
630
+
631
+ def change_tracked_paths(self, project_root: str) -> list[str]:
632
+ return _test_code_paths(project_root)
633
+
634
+ def _validate_current_test_code(
635
+ self,
636
+ project_root: str,
637
+ *,
638
+ allow_unchanged: bool,
639
+ ) -> tuple[bool, str]:
640
+ state = load_state(project_root)
641
+ if state is None:
642
+ return (False, "找不到当前工作流状态")
643
+ stage_state = state.stages.get(self.name())
644
+ if stage_state is None or stage_state.test_code_baseline_hash is None:
645
+ return (False, "缺少进入 test_code 时的测试代码基线")
646
+ if stage_state.non_test_code_baseline_hash is None:
647
+ return (False, "缺少进入 test_code 时的产品代码基线,请重新完成 test_code 讨论门禁")
648
+ if compute_non_test_code_snapshot_hash(project_root) != stage_state.non_test_code_baseline_hash:
649
+ return (False, "test_code 阶段不能修改产品代码;产品代码变化应返回 impl")
650
+ topics = current_workflow_topics(project_root)
651
+ if not topics:
652
+ return (False, "当前工作流还没有确认验收主题")
653
+ try:
654
+ automated_items = automated_test_items(project_root, topics)
655
+ except ValueError as exc:
656
+ return (False, str(exc))
657
+ current_test_code_hash = compute_test_code_snapshot_hash(project_root)
658
+ accepted_hash = stage_state.existing_test_code_accepted_hash
659
+ if accepted_hash is not None and current_test_code_hash != accepted_hash:
660
+ return (False, "既有测试代码确认后又发生变化,需要重新确认或重新修改测试代码")
661
+ if (
662
+ automated_items
663
+ and current_test_code_hash == stage_state.test_code_baseline_hash
664
+ and accepted_hash != current_test_code_hash
665
+ and not allow_unchanged
666
+ ):
667
+ return (
668
+ False,
669
+ "存在自动化测试项,但测试代码没有变化;如果当前测试代码已经覆盖最新测试计划,"
670
+ "请由用户执行 workflow gate test_code --accept-existing-test-code 明确确认",
671
+ )
672
+ marker_ok, marker_detail = validate_workflow_test_markers(project_root, topics)
673
+ if not marker_ok:
674
+ return (False, marker_detail)
675
+ state_ok, state_detail = validate_downstream_traceability(
676
+ project_root,
677
+ state.workflow_id,
678
+ topics,
679
+ )
680
+ if not state_ok:
681
+ return (False, state_detail)
682
+ if not automated_items:
683
+ return (True, f"{len(topics)} 个验收主题都没有自动化测试项,无需新增测试代码")
684
+ return (
685
+ True,
686
+ f"测试代码已覆盖 {len(automated_items)} 个自动化测试项;{marker_detail}",
687
+ )
688
+
689
+ def validate_existing_test_code(self, project_root: str) -> tuple[bool, str]:
690
+ """校验当前测试代码是否可以作为最新测试计划的既有实现继续使用。"""
691
+ return self._validate_current_test_code(project_root, allow_unchanged=True)
692
+
693
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
694
+ return self._validate_current_test_code(project_root, allow_unchanged=False)
695
+
696
+ def instruction(self) -> str:
697
+ return (
698
+ "测试代码阶段:先读取验收计划、测试计划、实施记录和真实代码,和用户确认每个测试项的代码落点;"
699
+ "再编写带 Workflow-Test 标识的测试代码。可以运行与当前编写内容直接相关的单个测试作为开发反馈,"
700
+ "但不生成正式机器记录、不写主题测试结果,也不写主题验收结果;正式执行只发生在 test_execution 阶段"
701
+ )
702
+
703
+
704
+ # 测试执行阶段:运行已写好的测试代码,并留下主题测试结果。
705
+ class TestExecutionStage(StageStrategy):
706
+ def name(self) -> str:
707
+ return "test_execution"
708
+
709
+ def artifact_paths(self) -> list[str]:
710
+ return ["qa/"]
711
+
712
+ def role_doc_path(self) -> str | None:
713
+ return None
714
+
715
+ def prompt_doc_path(self) -> str | None:
716
+ return "Template_Repository/qa/test.md"
717
+
718
+ def standard_doc_path(self) -> str | None:
719
+ return "Standardized_Repository/qa/test.md"
720
+
721
+ def discussion_validate(self, project_root: str, workflow_state) -> tuple[bool, str]:
722
+ """第一道门只确认测试任务已经登记,不在这里执行测试。"""
723
+ return test_execution_mod.validate_prepared_tasks(project_root, workflow_state)
724
+
725
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
726
+ state = load_state(project_root)
727
+ if state is None:
728
+ return (False, "找不到当前工作流状态")
729
+ topics = current_workflow_topics(project_root)
730
+ if not topics:
731
+ return (False, "当前工作流还没有确认验收主题")
732
+ if state.verification.test_code_hash is None:
733
+ return (False, "缺少 test_code 确认后的测试代码哈希,不能执行正式测试")
734
+ if compute_test_code_snapshot_hash(project_root) != state.verification.test_code_hash:
735
+ return (False, "test_code 确认后测试代码或测试配置发生变化,必须返回 test_code")
736
+ try:
737
+ test_topics = automated_topics(project_root, topics)
738
+ except ValueError as exc:
739
+ return (False, str(exc))
740
+ missing = missing_topic_documents(project_root, "test_result", test_topics)
741
+ if missing:
742
+ return (False, f"缺少主题测试结果: {missing};先执行测试并记录正式结果")
743
+ trace_ok, trace_detail = validate_downstream_traceability(
744
+ project_root,
745
+ state.workflow_id,
746
+ topics,
747
+ )
748
+ if not trace_ok:
749
+ return (False, trace_detail)
750
+ return validate_test_execution_results(project_root, state.workflow_id, topics)
751
+
752
+ def instruction(self) -> str:
753
+ return (
754
+ "测试执行阶段:先和用户逐项确认测试入口、前置测试项、真实命令、工作目录、运行环境和超时时间;"
755
+ "确认后用 workflow test prepare 登记,再用 workflow test run 执行。"
756
+ "执行失败不生成正式主题测试结果,先定位问题并按规则返回对应阶段。"
757
+ )
758
+
759
+
760
+ # 主题验收阶段:测试结果通过后,按验收条件核对用户结果。
761
+ class TopicAcceptanceStage(StageStrategy):
762
+ def name(self) -> str:
763
+ return "topic_acceptance"
764
+
765
+ def artifact_paths(self) -> list[str]:
766
+ return ["acceptance/"]
767
+
768
+ def role_doc_path(self) -> str | None:
769
+ return None
770
+
771
+ def prompt_doc_path(self) -> str | None:
772
+ return "Template_Repository/acceptance/acceptance_result.md"
773
+
774
+ def standard_doc_path(self) -> str | None:
775
+ return "Standardized_Repository/acceptance/acceptance.md"
776
+
777
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
778
+ state = load_state(project_root)
779
+ if state is None:
780
+ return (False, "找不到当前工作流状态")
781
+ topics = current_workflow_topics(project_root)
782
+ if not topics:
783
+ return (False, "当前工作流还没有确认验收主题")
784
+ test_ok, test_detail = validate_test_execution_results(
785
+ project_root,
786
+ state.workflow_id,
787
+ topics,
788
+ )
789
+ if not test_ok:
790
+ return (False, f"主题测试未全部通过,不能开始主题验收: {test_detail}")
791
+ missing = missing_topic_documents(project_root, "acceptance_result", topics)
792
+ if missing:
793
+ return (False, f"缺少主题验收结果: {missing}")
794
+ trace_ok, trace_detail = validate_downstream_traceability(
795
+ project_root,
796
+ state.workflow_id,
797
+ topics,
798
+ )
799
+ if not trace_ok:
800
+ return (False, trace_detail)
801
+ return validate_topic_acceptance_results(project_root, state.workflow_id, topics)
802
+
803
+ def instruction(self) -> str:
804
+ return (
805
+ "主题验收阶段:先确认对应主题的测试结果已经通过,再按主题验收计划逐条核对用户结果;"
806
+ "只有全部验收条件通过后才产出 acceptance/<主题文件标识>_验收结果.md;"
807
+ "任一条件未通过、无法验证或阻塞时不生成正式结果,调查原因后返回对应阶段;"
808
+ "只取消一个功能时退回 spec 并定向删除对应代码,只有整个工作流不再继续时才执行 workflow abort"
809
+ )
810
+
811
+
812
+ # 兼容旧代码导入;正式路径只使用 test_execution 和 topic_acceptance。
813
+ TestStage = TestExecutionStage
814
+ AcceptanceStage = TopicAcceptanceStage
815
+
816
+
817
+ class RegressionTestStage(StageStrategy):
818
+ """全部主题完成后,对合并后的完整代码执行最终全量回归。"""
819
+
820
+ def name(self) -> str:
821
+ return "regression_test"
822
+
823
+ def artifact_paths(self) -> list[str]:
824
+ return []
825
+
826
+ def role_doc_path(self) -> str | None:
827
+ return None
828
+
829
+ def prompt_doc_path(self) -> str | None:
830
+ return None
831
+
832
+ def standard_doc_path(self) -> str | None:
833
+ return None
834
+
835
+ def discussion_validate(self, project_root: str, workflow_state) -> tuple[bool, str]:
836
+ """第一道门只确认全部主题通过、当前系统入口和运行条件,不执行回归。"""
837
+ topics = current_workflow_topics(project_root)
838
+ acceptance_ok, acceptance_detail = validate_topic_acceptance_results(
839
+ project_root,
840
+ workflow_state.workflow_id,
841
+ topics,
842
+ )
843
+ if not acceptance_ok:
844
+ return (False, f"主题验收尚未全部通过,不能进入最终全量回归: {acceptance_detail}")
845
+ argv, entry_detail = test_runner_mod.resolve_regression_entry(project_root)
846
+ if argv is None:
847
+ return (False, f"项目全量测试入口未就绪:{entry_detail}")
848
+ return (True, f"全部主题已通过,当前平台全量入口就绪: {argv}")
849
+
850
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
851
+ state = load_state(project_root)
852
+ if state is None:
853
+ return (False, "找不到当前工作流状态")
854
+ topics = current_workflow_topics(project_root)
855
+ trace_ok, trace_detail = validate_downstream_traceability(
856
+ project_root,
857
+ state.workflow_id,
858
+ topics,
859
+ )
860
+ if not trace_ok:
861
+ return (False, trace_detail)
862
+ return validate_final_regression_state(project_root, state.workflow_id)
863
+
864
+ def instruction(self) -> str:
865
+ return (
866
+ "最终全量回归阶段:全部主题完成后,程序在最新完整代码上执行一次项目配置的统一测试入口;"
867
+ "退出码为 0 才算回归通过,完整机器事实写入 state.json、journal 和需求交付追踪表。"
868
+ )
869
+
870
+
871
+ class OverallAcceptanceStage(StageStrategy):
872
+ """最终全量回归通过后,由用户确认整个需求的结果。"""
873
+
874
+ def name(self) -> str:
875
+ return "overall_acceptance"
876
+
877
+ def artifact_paths(self) -> list[str]:
878
+ return []
879
+
880
+ def role_doc_path(self) -> str | None:
881
+ return None
882
+
883
+ def prompt_doc_path(self) -> str | None:
884
+ return None
885
+
886
+ def standard_doc_path(self) -> str | None:
887
+ return None
888
+
889
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
890
+ state = load_state(project_root)
891
+ if state is None:
892
+ return (False, "找不到当前工作流状态")
893
+ topics = current_workflow_topics(project_root)
894
+ trace_ok, trace_detail = validate_downstream_traceability(
895
+ project_root,
896
+ state.workflow_id,
897
+ topics,
898
+ )
899
+ if not trace_ok:
900
+ return (False, trace_detail)
901
+ return validate_overall_acceptance_prerequisites(
902
+ project_root,
903
+ state.workflow_id,
904
+ topics,
905
+ )
906
+
907
+ def instruction(self) -> str:
908
+ return (
909
+ "整体验收阶段:代码复核全部主题验收和最终全量回归已经通过,"
910
+ "再由用户确认全部主题组合后是否完成需求;本阶段不生成新的结果文档。"
911
+ )
912
+
913
+
914
+ # 最终设计同步 stage:核对产品、功能、架构和真实代码,并更新最终架构文档
915
+ class UpdateCodeDesignStage(StageStrategy):
916
+ def name(self) -> str:
917
+ return "update_code_design"
918
+
919
+ def artifact_paths(self) -> list[str]:
920
+ return [artifact_paths_mod.CODE_DESIGN_DOC]
921
+
922
+ def role_doc_path(self) -> str | None:
923
+ return None
924
+
925
+ def prompt_doc_path(self) -> str | None:
926
+ return "Template_Repository/code_design/code_design.md"
927
+
928
+ def standard_doc_path(self) -> str | None:
929
+ return "Standardized_Repository/code_design/update_code_design.md"
930
+
931
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
932
+ architecture_rel = artifact_paths_mod.CODE_DESIGN_DOC
933
+ if not os.path.isfile(os.path.join(project_root, architecture_rel)):
934
+ return (False, f"{architecture_rel} 不存在")
935
+ changed_ok, changed_detail, changed_paths = changed_stage_paths(
936
+ project_root,
937
+ self.name(),
938
+ self.change_tracked_paths(project_root),
939
+ )
940
+ if not changed_ok:
941
+ return (False, changed_detail)
942
+ normalized_changed = [path.replace(os.sep, "/") for path in changed_paths]
943
+ changed_product_docs = [
944
+ path
945
+ for path in normalized_changed
946
+ if path == artifact_paths_mod.PRODUCT_OVERVIEW_DOC
947
+ or path.startswith("spec/功能_")
948
+ ]
949
+ if changed_product_docs:
950
+ return (
951
+ False,
952
+ "产品总说明或功能文档在 update_code_design 阶段发生变化:"
953
+ f"{changed_product_docs};功能变化必须返回 spec,不能在最终设计同步阶段直接修改",
954
+ )
955
+ if architecture_rel not in normalized_changed:
956
+ return (
957
+ False,
958
+ f"最终设计同步没有更新 {architecture_rel};"
959
+ "即使架构没有变化,也要写入本轮核对结论和真实代码映射",
960
+ )
961
+ state = load_state(project_root)
962
+ if state is None:
963
+ return (False, "找不到当前工作流状态")
964
+ topics = current_workflow_topics(project_root)
965
+ trace_ok, trace_detail = validate_downstream_traceability(
966
+ project_root,
967
+ state.workflow_id,
968
+ topics,
969
+ )
970
+ if not trace_ok:
971
+ return (False, trace_detail)
972
+ document_ok, document_detail = validate_final_code_design_document(
973
+ project_root,
974
+ state.workflow_id,
975
+ )
976
+ if not document_ok:
977
+ return (False, document_detail)
978
+ return (True, f"{document_detail};追踪表已准备好记录最终代码设计")
979
+
980
+ def change_tracked_paths(self, project_root: str) -> list[str]:
981
+ return [
982
+ artifact_paths_mod.CODE_DESIGN_DOC,
983
+ *get_linked_product_design_paths(project_root),
984
+ ]
985
+
986
+ def instruction(self) -> str:
987
+ return (
988
+ "最终设计同步:逐项核对所有当前产品功能、真实代码符号和验证依据;"
989
+ "架构有变化时更新架构和功能到代码的映射,架构无变化时记录核对结论;"
990
+ "发现功能变化返回 spec,发现代码未实现返回 impl;确认无未处理差异后,"
991
+ f"写入/更新 {artifact_paths_mod.CODE_DESIGN_DOC}"
992
+ )
993
+
994
+
995
+ # 项目设计架构初始化 stage:根据现有代码及可运行行为一次建立设计文档
996
+ class ProjectDesignInitStage(StageStrategy):
997
+ def name(self) -> str:
998
+ return "project_design_init"
999
+
1000
+ def artifact_paths(self) -> list[str]:
1001
+ return [
1002
+ artifact_paths_mod.PRODUCT_OVERVIEW_DOC,
1003
+ artifact_paths_mod.CODE_DESIGN_DOC,
1004
+ PROJECT_INIT_EVIDENCE_PATH,
1005
+ ]
1006
+
1007
+ def role_doc_path(self) -> str | None:
1008
+ return None
1009
+
1010
+ def prompt_doc_path(self) -> str | None:
1011
+ return "Template_Repository/code_design/project_design_init_evidence.md"
1012
+
1013
+ def standard_doc_path(self) -> str | None:
1014
+ return "Standardized_Repository/code_design/project_design_init.md"
1015
+
1016
+ def additional_doc_paths(self) -> list[tuple[str, str]]:
1017
+ return [
1018
+ ("Template_Repository/spec/spec.md", "Standardized_Repository/spec/spec.md"),
1019
+ ("Template_Repository/code_design/code_design.md", "Standardized_Repository/code_design/code_design.md"),
1020
+ ]
1021
+
1022
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
1023
+ overview_rel = artifact_paths_mod.PRODUCT_OVERVIEW_DOC
1024
+ architecture_rel = artifact_paths_mod.CODE_DESIGN_DOC
1025
+ missing = []
1026
+ if not os.path.exists(os.path.join(project_root, overview_rel)):
1027
+ missing.append(overview_rel)
1028
+ if not os.path.exists(os.path.join(project_root, architecture_rel)):
1029
+ missing.append(architecture_rel)
1030
+ linked_features = [
1031
+ path
1032
+ for path in get_linked_product_design_paths(project_root)
1033
+ if path != overview_rel
1034
+ ]
1035
+ if not linked_features:
1036
+ missing.append("spec/功能_*.md")
1037
+ if not os.path.exists(os.path.join(project_root, PROJECT_INIT_EVIDENCE_PATH)):
1038
+ missing.append(PROJECT_INIT_EVIDENCE_PATH)
1039
+ if missing:
1040
+ return (False, f"产物未就绪: {missing}")
1041
+
1042
+ changed_ok, changed_detail, changed_paths = changed_stage_paths(
1043
+ project_root,
1044
+ self.name(),
1045
+ self.change_tracked_paths(project_root),
1046
+ )
1047
+ if not changed_ok:
1048
+ return (False, changed_detail)
1049
+
1050
+ normalized_changed = [path.replace(os.sep, "/") for path in changed_paths]
1051
+ product_changed = any(
1052
+ path == overview_rel or path.startswith("spec/功能_")
1053
+ for path in normalized_changed
1054
+ )
1055
+ architecture_changed = architecture_rel in normalized_changed
1056
+ evidence_changed = PROJECT_INIT_EVIDENCE_PATH.replace(os.sep, "/") in normalized_changed
1057
+ if not product_changed or not architecture_changed or not evidence_changed:
1058
+ return (
1059
+ False,
1060
+ "项目设计初始化必须在本阶段更新产品设计、代码设计和调查证据三类内容;"
1061
+ f"当前变化文件: {changed_paths}",
1062
+ )
1063
+
1064
+ state = load_state(project_root)
1065
+ if state is None:
1066
+ return (False, "找不到当前工作流状态")
1067
+ evidence_ok, evidence_detail = validate_project_design_init_evidence(
1068
+ project_root,
1069
+ state.workflow_id,
1070
+ )
1071
+ if not evidence_ok:
1072
+ return (False, evidence_detail)
1073
+ return (
1074
+ True,
1075
+ "项目设计初始化产物和调查证据有效: "
1076
+ f"产品总说明 + 代码架构设计 + {[os.path.basename(f) for f in linked_features]}; "
1077
+ f"{evidence_detail}",
1078
+ )
1079
+
1080
+ def change_tracked_paths(self, project_root: str) -> list[str]:
1081
+ return [
1082
+ artifact_paths_mod.CODE_DESIGN_DOC,
1083
+ PROJECT_INIT_EVIDENCE_PATH,
1084
+ *get_linked_product_design_paths(project_root),
1085
+ ]
1086
+
1087
+ def instruction(self) -> str:
1088
+ return (
1089
+ "已有项目设计初始化:必须查看代码和测试,具备安全条件时实际运行;"
1090
+ f"一次建立相互一致的产品文档、代码架构文档和 {PROJECT_INIT_EVIDENCE_PATH} 调查证据"
1091
+ )
1092
+
1093
+
1094
+ # 设计期架构修订 stage:按变更后的产品设计改代码架构设计
1095
+ class ReviseCodeDesignStage(StageStrategy):
1096
+ def name(self) -> str:
1097
+ return "revise_code_design"
1098
+
1099
+ def artifact_paths(self) -> list[str]:
1100
+ return [artifact_paths_mod.CODE_DESIGN_DOC]
1101
+
1102
+ def role_doc_path(self) -> str | None:
1103
+ return None
1104
+
1105
+ def prompt_doc_path(self) -> str | None:
1106
+ return "Template_Repository/code_design/code_design.md"
1107
+
1108
+ def standard_doc_path(self) -> str | None:
1109
+ return "Standardized_Repository/code_design/revise_code_design.md"
1110
+
1111
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
1112
+ architecture_rel = artifact_paths_mod.CODE_DESIGN_DOC
1113
+ if not os.path.isfile(os.path.join(project_root, architecture_rel)):
1114
+ return (False, f"{architecture_rel} 不存在")
1115
+ changed_ok, changed_detail, _ = changed_stage_paths(
1116
+ project_root,
1117
+ self.name(),
1118
+ self.change_tracked_paths(project_root),
1119
+ )
1120
+ if not changed_ok:
1121
+ return (False, changed_detail)
1122
+ return (True, f"{architecture_rel} 已按本阶段产品设计修改")
1123
+
1124
+ def change_tracked_paths(self, project_root: str) -> list[str]:
1125
+ return [artifact_paths_mod.CODE_DESIGN_DOC]
1126
+
1127
+ def instruction(self) -> str:
1128
+ return f"设计期架构修订:按变更后的产品设计改 {artifact_paths_mod.CODE_DESIGN_DOC}"
1129
+
1130
+
1131
+ # 复现 stage:和用户讨论 bug 现象、复现步骤
1132
+ class ReproduceStage(StageStrategy):
1133
+ def name(self) -> str:
1134
+ return "reproduce"
1135
+
1136
+ def artifact_paths(self) -> list[str]:
1137
+ return [artifact_paths_mod.BUG_INDEX_DOC, "bug/"]
1138
+
1139
+ def role_doc_path(self) -> str | None:
1140
+ return None
1141
+
1142
+ def prompt_doc_path(self) -> str | None:
1143
+ return "Template_Repository/reproduce/reproduce.md"
1144
+
1145
+ def standard_doc_path(self) -> str | None:
1146
+ return "Standardized_Repository/reproduce/reproduce.md"
1147
+
1148
+ def code_validate(self, project_root: str) -> tuple[bool, str]:
1149
+ bug_dir = os.path.join(project_root, "bug")
1150
+ if not os.path.exists(bug_dir):
1151
+ return (False, "bug/ 目录不存在")
1152
+ index_path = os.path.join(project_root, artifact_paths_mod.BUG_INDEX_DOC)
1153
+ if not os.path.isfile(index_path):
1154
+ return (False, f"{artifact_paths_mod.BUG_INDEX_DOC} 不存在")
1155
+ md_files = [
1156
+ f
1157
+ for f in os.listdir(bug_dir)
1158
+ if f.endswith(".md") and f != "索引.md"
1159
+ ]
1160
+ if not md_files:
1161
+ return (False, "bug/ 下没有缺陷记录文档")
1162
+
1163
+ changed_ok, changed_detail, changed_paths = changed_stage_paths(
1164
+ project_root,
1165
+ self.name(),
1166
+ self.change_tracked_paths(project_root),
1167
+ )
1168
+ if not changed_ok:
1169
+ return (False, changed_detail)
1170
+
1171
+ state = load_state(project_root)
1172
+ if state is None:
1173
+ return (False, "找不到当前工作流状态")
1174
+ return validate_reproduce_documents(project_root, changed_paths, state.workflow_id)
1175
+
1176
+ def change_tracked_paths(self, project_root: str) -> list[str]:
1177
+ bug_dir = os.path.join(project_root, "bug")
1178
+ bug_paths = []
1179
+ if os.path.isdir(bug_dir):
1180
+ bug_paths = [
1181
+ os.path.join("bug", filename)
1182
+ for filename in os.listdir(bug_dir)
1183
+ if filename.endswith(".md") and filename != "索引.md"
1184
+ ]
1185
+ return [artifact_paths_mod.BUG_INDEX_DOC, *bug_paths]
1186
+
1187
+ def instruction(self) -> str:
1188
+ return (
1189
+ "缺陷复现阶段:使用真实环境和真实输入复现缺陷并确认根因,"
1190
+ f"产出 bug/缺陷_<缺陷文件标识>.md 并更新 {artifact_paths_mod.BUG_INDEX_DOC}"
1191
+ )