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,533 @@
1
+ """需求交付追踪表的读取和阶段更新。
2
+
3
+ 追踪表是项目根下的 ``需求交付追踪表.md``。本模块只更新当前工作流中
4
+ 当前阶段负责的列,不重写旧工作流,也不修改验收计划文件本身。
5
+ 通过已登记的主题文件标识识别当前工作流的每一行,不从文件名反推主题。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+
13
+ from . import artifact_paths as artifact_paths_mod
14
+ from .state import load_state
15
+ from .test_mapping import criterion_requires_test_code, parse_test_plan_items
16
+ from .topic import topic_paths
17
+
18
+
19
+ TRACEABILITY_PATH = artifact_paths_mod.TRACEABILITY_DOC
20
+ TRACEABILITY_HEADERS = [
21
+ "需求来源与设计依据",
22
+ "验收主题",
23
+ "验收条件",
24
+ "测试项",
25
+ "实施计划与任务",
26
+ "实施记录与代码",
27
+ "测试结果",
28
+ "验收结果",
29
+ "更新后的代码设计",
30
+ ]
31
+
32
+ _INITIAL_VALUES = {"待制定", "待执行", "待更新"}
33
+ _INITIAL_ROW_VALUES = ["待制定", "待制定", "待执行", "待执行", "待执行", "待更新"]
34
+ _CRITERION_ID_RE = re.compile(r"\b(AC-\d{2,})\b")
35
+ _REGRESSION_CONCLUSION_RE = re.compile(
36
+ r"最终全量回归:通过(?:(机器记录 [^)]+))?"
37
+ )
38
+ _OVERALL_ACCEPTANCE_CONCLUSION_RE = re.compile(r"整体验收:用户已确认")
39
+
40
+
41
+ def _trace_relative_path(project_root: str) -> str:
42
+ """返回唯一受支持的中文需求交付追踪表路径。"""
43
+ return TRACEABILITY_PATH
44
+
45
+
46
+ def _workflow_heading_pattern(workflow_id: str) -> re.Pattern[str]:
47
+ return re.compile(
48
+ rf"^##\s+{re.escape(workflow_id)}\s*$\n(.*?)(?=^##\s+|\Z)",
49
+ re.MULTILINE | re.DOTALL,
50
+ )
51
+
52
+
53
+ def _split_table_line(line: str) -> list[str] | None:
54
+ stripped = line.strip()
55
+ if not stripped.startswith("|") or not stripped.endswith("|"):
56
+ return None
57
+ cells = [cell.strip() for cell in stripped.strip("|").split("|")]
58
+ if not cells or all(re.fullmatch(r"[-:]+", cell) for cell in cells):
59
+ return None
60
+ return cells
61
+
62
+
63
+ def _format_table_line(cells: list[str]) -> str:
64
+ return "| " + " | ".join(cells) + " |"
65
+
66
+
67
+ def _read_traceability(project_root: str) -> tuple[str, str]:
68
+ relative_path = _trace_relative_path(project_root)
69
+ path = Path(project_root) / relative_path
70
+ if not path.is_file():
71
+ raise ValueError(f"{TRACEABILITY_PATH} 不存在")
72
+ return path.read_text(encoding="utf-8"), relative_path
73
+
74
+
75
+ def _workflow_match(content: str, workflow_id: str) -> re.Match[str]:
76
+ match = _workflow_heading_pattern(workflow_id).search(content)
77
+ if match is None:
78
+ raise ValueError(f"{TRACEABILITY_PATH} 缺少当前工作流章节: {workflow_id}")
79
+ return match
80
+
81
+
82
+ def _topic_markers(project_root: str, topic: str) -> list[str]:
83
+ """使用当前登记的中文验收计划路径识别主题行。"""
84
+ plan_path = topic_paths(project_root, topic)["acceptance_plan"]
85
+ return [plan_path]
86
+
87
+
88
+ def _topic_rows(
89
+ project_root: str,
90
+ section: str,
91
+ topic: str,
92
+ ) -> list[tuple[int, list[str]]]:
93
+ markers = _topic_markers(project_root, topic)
94
+ rows: list[tuple[int, list[str]]] = []
95
+ for index, line in enumerate(section.splitlines()):
96
+ cells = _split_table_line(line)
97
+ if cells is not None and len(cells) == 9 and any(
98
+ marker in cells[1] for marker in markers
99
+ ):
100
+ rows.append((index, cells))
101
+ return rows
102
+
103
+
104
+ def _replace_workflow_section(content: str, workflow_id: str, section: str) -> str:
105
+ match = _workflow_match(content, workflow_id)
106
+ return content[: match.start(1)] + section + content[match.end(1) :]
107
+
108
+
109
+ def _write_traceability(project_root: str, relative_path: str, content: str) -> None:
110
+ Path(project_root, relative_path).write_text(content, encoding="utf-8")
111
+
112
+
113
+ def _set_or_append_link(current: str, label: str, path: str) -> str:
114
+ """把阶段链接写入单元格;已经存在时不重复添加。"""
115
+ link = f"[{label}]({path})"
116
+ if link in current:
117
+ return current
118
+ if current in _INITIAL_VALUES:
119
+ return link
120
+ return f"{current}<br>{link}"
121
+
122
+
123
+ def _set_or_append_text(current: str, value: str) -> str:
124
+ if value in current:
125
+ return current
126
+ if current in _INITIAL_VALUES:
127
+ return value
128
+ return f"{current}<br>{value}"
129
+
130
+
131
+ def _remove_appended_text(current: str, pattern: re.Pattern[str], fallback: str) -> str:
132
+ """只删除匹配的阶段追加结论,保留同一单元格中的正式结果链接。"""
133
+ retained = [
134
+ part
135
+ for part in current.split("<br>")
136
+ if pattern.fullmatch(part.strip()) is None
137
+ ]
138
+ return "<br>".join(retained) if retained else fallback
139
+
140
+
141
+ def _criterion_id_from_cell(topic: str, criterion_cell: str) -> str:
142
+ match = _CRITERION_ID_RE.search(criterion_cell)
143
+ if match is None:
144
+ raise ValueError(f"{TRACEABILITY_PATH} 主题“{topic}”的验收条件列缺少 AC 编号")
145
+ return match.group(1)
146
+
147
+
148
+ def _test_plan_links(project_root: str, topic: str, criterion_id: str) -> str:
149
+ """只生成当前 AC 对应的测试计划和 TC 链接。"""
150
+ test_plan_path = topic_paths(project_root, topic)["test_plan"]
151
+ items = [
152
+ item
153
+ for item in parse_test_plan_items(project_root, topic)
154
+ if item.criterion_id == criterion_id
155
+ ]
156
+ if not items:
157
+ raise ValueError(f"{test_plan_path} 没有覆盖 {criterion_id} 的测试项")
158
+ links = [f"[测试计划](./{test_plan_path})"]
159
+ links.extend(
160
+ f"[{item.test_id} {item.test_name}](./{test_plan_path}#{item.test_id.lower()})"
161
+ for item in items
162
+ )
163
+ return "<br>".join(links)
164
+
165
+
166
+ def validate_structure(
167
+ project_root: str,
168
+ workflow_id: str,
169
+ topics: list[str] | None = None,
170
+ *,
171
+ require_initial_statuses: bool = False,
172
+ require_completed_updates: bool = False,
173
+ ) -> tuple[bool, str]:
174
+ """校验当前工作流章节、九列表格和主题行。"""
175
+ try:
176
+ content, _relative_path = _read_traceability(project_root)
177
+ match = _workflow_match(content, workflow_id)
178
+ except ValueError as exc:
179
+ return False, str(exc)
180
+
181
+ lines = match.group(1).splitlines()
182
+ header_found = False
183
+ rows: list[list[str]] = []
184
+ for line in lines:
185
+ cells = _split_table_line(line)
186
+ if cells is None:
187
+ continue
188
+ if cells == TRACEABILITY_HEADERS:
189
+ header_found = True
190
+ continue
191
+ if len(cells) == 9 and cells[0] != "需求来源与设计依据":
192
+ rows.append(cells)
193
+
194
+ if not header_found:
195
+ return False, f"{TRACEABILITY_PATH} 当前工作流章节缺少九列表头"
196
+ if not rows:
197
+ return False, f"{TRACEABILITY_PATH} 当前工作流章节没有九列交付记录"
198
+
199
+ expected_topics = topics or []
200
+ for topic in expected_topics:
201
+ markers = _topic_markers(project_root, topic)
202
+ topic_rows = [
203
+ row for row in rows if any(marker in row[1] for marker in markers)
204
+ ]
205
+ if not topic_rows:
206
+ return False, f"{TRACEABILITY_PATH} 缺少主题“{topic}”的交付记录"
207
+ if any(not all(cell.strip() for cell in row) for row in topic_rows):
208
+ return False, f"{TRACEABILITY_PATH} 主题“{topic}”存在空单元格"
209
+ if require_initial_statuses:
210
+ expected = ["待制定", "待制定", "待执行", "待执行", "待执行", "待更新"]
211
+ for row in topic_rows:
212
+ if row[3:] != expected:
213
+ return False, f"{TRACEABILITY_PATH} 主题“{topic}”尚未保持初始状态: {expected}"
214
+ if require_completed_updates:
215
+ for row in topic_rows:
216
+ if any(value in _INITIAL_VALUES for value in row[3:]):
217
+ return False, f"{TRACEABILITY_PATH} 主题“{topic}”仍有未更新的阶段列"
218
+
219
+ return True, f"{TRACEABILITY_PATH} 当前工作流包含 {len(rows)} 条九列交付记录"
220
+
221
+
222
+ def _update_stage_rows(
223
+ project_root: str,
224
+ workflow_id: str,
225
+ topics: list[str],
226
+ column_index: int,
227
+ value_factory,
228
+ ) -> str:
229
+ content, relative_path = _read_traceability(project_root)
230
+ match = _workflow_match(content, workflow_id)
231
+ section_lines = match.group(1).splitlines()
232
+ changed = 0
233
+
234
+ for topic in topics:
235
+ rows = _topic_rows(project_root, "\n".join(section_lines), topic)
236
+ if not rows:
237
+ raise ValueError(f"{TRACEABILITY_PATH} 缺少主题“{topic}”的九列记录")
238
+ for line_index, cells in rows:
239
+ cells[column_index] = value_factory(topic, cells)
240
+ section_lines[line_index] = _format_table_line(cells)
241
+ changed += 1
242
+
243
+ updated_section = "\n".join(section_lines)
244
+ updated = _replace_workflow_section(content, workflow_id, updated_section)
245
+ if updated != content:
246
+ _write_traceability(project_root, relative_path, updated)
247
+ return f"已更新 {TRACEABILITY_PATH} 当前工作流 {changed} 条记录"
248
+
249
+
250
+ def reset_after_upstream_invalidation(
251
+ project_root: str,
252
+ workflow_id: str,
253
+ topics: list[str],
254
+ stage_name: str,
255
+ ) -> str:
256
+ """清理当前工作流中已经失效的下游追踪列。
257
+
258
+ 验收计划或测试计划变化后,原来的下游链接不能继续表示有效结果。
259
+ 这里只改当前工作流,不改旧工作流;后续阶段重新确认时会再写回自己的列。
260
+ """
261
+
262
+ reset_from = {
263
+ "acceptance_plan": 0,
264
+ "test_plan": 0,
265
+ }.get(stage_name)
266
+ if reset_from is None or not topics:
267
+ return f"{TRACEABILITY_PATH} 无需重置阶段 {stage_name} 的下游列"
268
+
269
+ if not (Path(project_root) / _trace_relative_path(project_root)).is_file():
270
+ return f"{TRACEABILITY_PATH} 不存在,无需重置阶段 {stage_name} 的下游列"
271
+
272
+ content, relative_path = _read_traceability(project_root)
273
+ match = _workflow_match(content, workflow_id)
274
+ section_lines = match.group(1).splitlines()
275
+ changed = 0
276
+
277
+ for topic in topics:
278
+ rows = _topic_rows(project_root, "\n".join(section_lines), topic)
279
+ for line_index, cells in rows:
280
+ for column_index in range(3 + reset_from, 9):
281
+ cells[column_index] = _INITIAL_ROW_VALUES[column_index - 3]
282
+ section_lines[line_index] = _format_table_line(cells)
283
+ changed += 1
284
+
285
+ if changed == 0:
286
+ return f"{TRACEABILITY_PATH} 当前工作流没有可重置的主题记录"
287
+
288
+ updated_section = "\n".join(section_lines)
289
+ updated = _replace_workflow_section(content, workflow_id, updated_section)
290
+ if updated != content:
291
+ _write_traceability(project_root, relative_path, updated)
292
+ return f"已重置 {TRACEABILITY_PATH} 当前工作流 {changed} 条失效下游记录"
293
+
294
+
295
+ # 各返回目标只重置确实失效的后续列。
296
+ _RETURN_RESET_COLUMNS: dict[str, dict[int, str]] = {
297
+ "spec": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
298
+ "code_design": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
299
+ "revise_code_design": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
300
+ "project_design_init": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
301
+ "reproduce": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
302
+ "spike": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
303
+ "acceptance_plan": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
304
+ "test_plan": {3: "待制定", 4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
305
+ "impl": {4: "待制定", 5: "待执行", 6: "待执行", 7: "待执行", 8: "待更新"},
306
+ "test_code": {6: "待执行", 7: "待执行", 8: "待更新"},
307
+ "test_execution": {6: "待执行", 7: "待执行", 8: "待更新"},
308
+ "topic_acceptance": {7: "待执行", 8: "待更新"},
309
+ "regression_test": {},
310
+ "overall_acceptance": {},
311
+ "update_code_design": {8: "待更新"},
312
+ }
313
+ _RETURN_REMOVE_CONCLUSIONS: dict[str, tuple[tuple[int, re.Pattern[str], str], ...]] = {
314
+ "regression_test": (
315
+ (6, _REGRESSION_CONCLUSION_RE, "待执行"),
316
+ (7, _OVERALL_ACCEPTANCE_CONCLUSION_RE, "待执行"),
317
+ ),
318
+ "overall_acceptance": (
319
+ (7, _OVERALL_ACCEPTANCE_CONCLUSION_RE, "待执行"),
320
+ ),
321
+ }
322
+
323
+
324
+ def reset_topics_for_return(
325
+ project_root: str,
326
+ workflow_id: str,
327
+ topics: list[str],
328
+ target_stage: str,
329
+ ) -> str:
330
+ """流程退回时,只清理受影响主题从目标阶段开始的当前交付状态。
331
+
332
+ 支持当前路径中的任意真实上游;独立主题的行保持不变。
333
+ """
334
+ columns = _RETURN_RESET_COLUMNS.get(target_stage)
335
+ if columns is None:
336
+ raise ValueError(f"不支持从追踪表退回阶段: {target_stage}")
337
+ conclusion_rules = _RETURN_REMOVE_CONCLUSIONS.get(target_stage, ())
338
+ if not columns and not conclusion_rules:
339
+ return f"{TRACEABILITY_PATH} 阶段 {target_stage} 没有需要重置的追踪列"
340
+ content, relative_path = _read_traceability(project_root)
341
+ match = _workflow_match(content, workflow_id)
342
+ section_lines = match.group(1).splitlines()
343
+ changed_lines: set[int] = set()
344
+ for topic in topics:
345
+ rows = _topic_rows(project_root, "\n".join(section_lines), topic)
346
+ if not rows:
347
+ raise ValueError(f"{TRACEABILITY_PATH} 缺少主题“{topic}”的九列记录")
348
+ for line_index, cells in rows:
349
+ original_cells = list(cells)
350
+ for column_index, value in columns.items():
351
+ cells[column_index] = value
352
+ if cells != original_cells:
353
+ section_lines[line_index] = _format_table_line(cells)
354
+ changed_lines.add(line_index)
355
+
356
+ # 最终回归和整体验收是整轮唯一的全局结论。状态失效时必须从当前工作流
357
+ # 每一行删除该结论,不能只删用户列出的直接受影响主题;主题自身的结果链接保留。
358
+ if conclusion_rules:
359
+ for line_index, line in enumerate(section_lines):
360
+ cells = _split_table_line(line)
361
+ if cells is None or len(cells) != 9 or cells == TRACEABILITY_HEADERS:
362
+ continue
363
+ original_cells = list(cells)
364
+ for column_index, pattern, fallback in conclusion_rules:
365
+ cells[column_index] = _remove_appended_text(
366
+ cells[column_index],
367
+ pattern,
368
+ fallback,
369
+ )
370
+ if cells != original_cells:
371
+ section_lines[line_index] = _format_table_line(cells)
372
+ changed_lines.add(line_index)
373
+ updated = _replace_workflow_section(content, workflow_id, "\n".join(section_lines))
374
+ if updated != content:
375
+ _write_traceability(project_root, relative_path, updated)
376
+ return f"已重置 {TRACEABILITY_PATH} 中 {len(changed_lines)} 条失效记录"
377
+
378
+
379
+ def reset_topic_test_results(
380
+ project_root: str,
381
+ workflow_id: str,
382
+ topics: list[str],
383
+ ) -> str:
384
+ """主题测试重新执行前,只把受影响主题的测试结果列恢复为待执行。"""
385
+ trace_path = Path(project_root) / _trace_relative_path(project_root)
386
+ if not trace_path.is_file() or not topics:
387
+ return f"{TRACEABILITY_PATH} 不存在或没有受影响主题,无需重置测试结果"
388
+
389
+ content, relative_path = _read_traceability(project_root)
390
+ match = _workflow_match(content, workflow_id)
391
+ section_lines = match.group(1).splitlines()
392
+ changed = 0
393
+ for topic in topics:
394
+ rows = _topic_rows(project_root, "\n".join(section_lines), topic)
395
+ if not rows:
396
+ raise ValueError(f"{TRACEABILITY_PATH} 缺少主题“{topic}”的九列记录")
397
+ for line_index, cells in rows:
398
+ cells[6] = "待执行"
399
+ section_lines[line_index] = _format_table_line(cells)
400
+ changed += 1
401
+
402
+ updated = _replace_workflow_section(content, workflow_id, "\n".join(section_lines))
403
+ if updated != content:
404
+ _write_traceability(project_root, relative_path, updated)
405
+ return f"已重置 {TRACEABILITY_PATH} 中 {changed} 条主题测试结果"
406
+
407
+
408
+ def update_for_stage(
409
+ project_root: str,
410
+ workflow_id: str,
411
+ topics: list[str],
412
+ stage_name: str,
413
+ ) -> str:
414
+ """更新一个阶段负责的追踪列。"""
415
+ if not topics:
416
+ raise ValueError("当前工作流没有验收主题,不能更新需求交付追踪表")
417
+
418
+ if stage_name == "test_plan":
419
+ return _update_stage_rows(
420
+ project_root,
421
+ workflow_id,
422
+ topics,
423
+ 3,
424
+ lambda topic, cells: _set_or_append_text(
425
+ cells[3],
426
+ _test_plan_links(
427
+ project_root,
428
+ topic,
429
+ _criterion_id_from_cell(topic, cells[2]),
430
+ ),
431
+ ),
432
+ )
433
+
434
+ if stage_name == "impl":
435
+ plan_update = _update_stage_rows(
436
+ project_root,
437
+ workflow_id,
438
+ topics,
439
+ 4,
440
+ lambda topic, cells: _set_or_append_link(
441
+ cells[4],
442
+ "实施前计划",
443
+ f"./{topic_paths(project_root, topic)['impl_doc']}#2-实施前计划",
444
+ ),
445
+ )
446
+ record_update = _update_stage_rows(
447
+ project_root,
448
+ workflow_id,
449
+ topics,
450
+ 5,
451
+ lambda topic, cells: _set_or_append_link(
452
+ cells[5],
453
+ "实施后记录",
454
+ f"./{topic_paths(project_root, topic)['impl_doc']}#3-实施后记录",
455
+ ),
456
+ )
457
+ return plan_update + ";" + record_update
458
+
459
+ if stage_name == "test_execution":
460
+ return _update_stage_rows(
461
+ project_root,
462
+ workflow_id,
463
+ topics,
464
+ 6,
465
+ lambda topic, cells: (
466
+ _set_or_append_link(
467
+ cells[6],
468
+ "测试结果",
469
+ f"./{topic_paths(project_root, topic)['test_result']}",
470
+ )
471
+ if criterion_requires_test_code(
472
+ project_root,
473
+ topic,
474
+ _criterion_id_from_cell(topic, cells[2]),
475
+ )
476
+ else _set_or_append_text(cells[6], "无自动化测试项,转主题验收")
477
+ ),
478
+ )
479
+
480
+ if stage_name == "topic_acceptance":
481
+ return _update_stage_rows(
482
+ project_root,
483
+ workflow_id,
484
+ topics,
485
+ 7,
486
+ lambda topic, cells: _set_or_append_link(
487
+ cells[7],
488
+ "主题验收结果",
489
+ f"./{topic_paths(project_root, topic)['acceptance_result']}",
490
+ ),
491
+ )
492
+
493
+ if stage_name == "regression_test":
494
+ # 最终回归写当前机器记录编号和通过结论,可以逐字段回查 state.json
495
+ state = load_state(project_root)
496
+ record_id = getattr(state.regression_test, "record_id", None) if state else None
497
+ conclusion = (
498
+ f"最终全量回归:通过(机器记录 {record_id})" if record_id else "最终全量回归:通过"
499
+ )
500
+ return _update_stage_rows(
501
+ project_root,
502
+ workflow_id,
503
+ topics,
504
+ 6,
505
+ lambda _topic, cells: _set_or_append_text(cells[6], conclusion),
506
+ )
507
+
508
+ if stage_name == "overall_acceptance":
509
+ return _update_stage_rows(
510
+ project_root,
511
+ workflow_id,
512
+ topics,
513
+ 7,
514
+ lambda _topic, cells: _set_or_append_text(
515
+ cells[7],
516
+ "整体验收:用户已确认",
517
+ ),
518
+ )
519
+
520
+ if stage_name == "update_code_design":
521
+ return _update_stage_rows(
522
+ project_root,
523
+ workflow_id,
524
+ topics,
525
+ 8,
526
+ lambda _topic, cells: _set_or_append_link(
527
+ cells[8],
528
+ "最终代码设计",
529
+ f"./{artifact_paths_mod.CODE_DESIGN_DOC}",
530
+ ),
531
+ )
532
+
533
+ raise ValueError(f"阶段 {stage_name} 没有追踪表更新规则")