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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from . import artifact_paths as artifact_paths_mod
|
|
6
|
+
from . import state as state_mod
|
|
7
|
+
from . import verification as verification_mod
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
FIELD_RE = re.compile(r"^- ([^:\n]+):\s*(.*)$")
|
|
11
|
+
INDEX_ITEM_RE = re.compile(r"^## (SP-\d{3})\s+(.+?)\s*$")
|
|
12
|
+
DETAIL_SECTION_RE = re.compile(r"^## ([1-8])\.\s+(.+?)\s*$")
|
|
13
|
+
MARKDOWN_LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
|
14
|
+
PLACEHOLDER_RE = re.compile(r"<[^>\n]+>")
|
|
15
|
+
|
|
16
|
+
INDEX_REQUIRED_FIELDS = [
|
|
17
|
+
"真实场景",
|
|
18
|
+
"要验证的不确定性",
|
|
19
|
+
"验证结果用于决定什么",
|
|
20
|
+
"结论文档",
|
|
21
|
+
"穿刺状态",
|
|
22
|
+
"是否阻塞后续",
|
|
23
|
+
"产品设计影响",
|
|
24
|
+
"代码设计影响",
|
|
25
|
+
"后续处理阶段",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
EXPECTED_DETAIL_SECTIONS = {
|
|
29
|
+
"1": "真实场景与不确定性",
|
|
30
|
+
"2": "验证结果用于决定什么",
|
|
31
|
+
"3": "已知事实与验证范围",
|
|
32
|
+
"4": "验证方法",
|
|
33
|
+
"5": "实际执行记录",
|
|
34
|
+
"6": "实际观察结果",
|
|
35
|
+
"7": "结论",
|
|
36
|
+
"8": "对后续工作的影响",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
METHOD_FIELDS = ["使用的方法", "临时内容位置", "执行步骤", "外部影响"]
|
|
40
|
+
EXECUTION_FIELDS = ["执行时间", "运行环境", "实际命令", "真实输入或样本", "执行失败"]
|
|
41
|
+
RESULT_FIELDS = ["结果状态", "是否阻塞后续", "已确认内容", "仍未确认内容"]
|
|
42
|
+
IMPACT_FIELDS = [
|
|
43
|
+
"产品设计影响",
|
|
44
|
+
"产品设计更新位置",
|
|
45
|
+
"代码设计影响",
|
|
46
|
+
"代码设计更新位置",
|
|
47
|
+
"剩余风险",
|
|
48
|
+
"后续处理阶段",
|
|
49
|
+
"后续需要检查什么",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
INDEX_STATUSES = {"待验证", "已确认", "限制已确认", "仍未确认"}
|
|
53
|
+
RESULT_STATUSES = {"已确认", "限制已确认", "仍未确认"}
|
|
54
|
+
YES_NO = {"是", "否"}
|
|
55
|
+
DESIGN_IMPACTS = {"需要修改", "无需修改"}
|
|
56
|
+
FOLLOW_UP_STAGES = {
|
|
57
|
+
"无",
|
|
58
|
+
"acceptance_plan",
|
|
59
|
+
"test_plan",
|
|
60
|
+
"impl",
|
|
61
|
+
"test_code",
|
|
62
|
+
"test_execution",
|
|
63
|
+
"topic_acceptance",
|
|
64
|
+
"regression_test",
|
|
65
|
+
"overall_acceptance",
|
|
66
|
+
"update_code_design",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class SpikeIndexItem:
|
|
72
|
+
item_id: str
|
|
73
|
+
name: str
|
|
74
|
+
fields: dict[str, str]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class SpikeDetail:
|
|
79
|
+
metadata: dict[str, str]
|
|
80
|
+
sections: dict[str, str]
|
|
81
|
+
section_fields: dict[str, dict[str, str]]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _read_text(path: str) -> str:
|
|
85
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
86
|
+
return f.read()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_fields(lines: list[str], context: str) -> tuple[dict[str, str], list[str]]:
|
|
90
|
+
fields: dict[str, str] = {}
|
|
91
|
+
errors = []
|
|
92
|
+
for line in lines:
|
|
93
|
+
match = FIELD_RE.match(line.strip())
|
|
94
|
+
if not match:
|
|
95
|
+
continue
|
|
96
|
+
key, value = match.groups()
|
|
97
|
+
if key in fields:
|
|
98
|
+
errors.append(f"{context} 重复字段:{key}")
|
|
99
|
+
continue
|
|
100
|
+
fields[key] = value.strip()
|
|
101
|
+
return fields, errors
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _missing_or_placeholder(value: str | None) -> bool:
|
|
105
|
+
if value is None or not value.strip():
|
|
106
|
+
return True
|
|
107
|
+
return PLACEHOLDER_RE.search(value) is not None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _require_fields(fields: dict[str, str], required: list[str], context: str) -> list[str]:
|
|
111
|
+
errors = []
|
|
112
|
+
for key in required:
|
|
113
|
+
if key not in fields:
|
|
114
|
+
errors.append(f"{context} 缺少字段:{key}")
|
|
115
|
+
elif _missing_or_placeholder(fields[key]):
|
|
116
|
+
errors.append(f"{context} 字段未填写:{key}")
|
|
117
|
+
return errors
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_spike_index(path: str) -> tuple[str | None, list[SpikeIndexItem], list[str]]:
|
|
121
|
+
lines = _read_text(path).splitlines()
|
|
122
|
+
item_starts = []
|
|
123
|
+
for index, line in enumerate(lines):
|
|
124
|
+
match = INDEX_ITEM_RE.match(line.strip())
|
|
125
|
+
if match:
|
|
126
|
+
item_starts.append((index, match.group(1), match.group(2).strip()))
|
|
127
|
+
|
|
128
|
+
errors = []
|
|
129
|
+
top_end = item_starts[0][0] if item_starts else len(lines)
|
|
130
|
+
top_fields, top_errors = _parse_fields(lines[:top_end], "穿刺清单")
|
|
131
|
+
errors.extend(top_errors)
|
|
132
|
+
workflow_id = top_fields.get("工作流编号")
|
|
133
|
+
if _missing_or_placeholder(workflow_id):
|
|
134
|
+
errors.append("穿刺清单缺少有效的工作流编号")
|
|
135
|
+
|
|
136
|
+
items = []
|
|
137
|
+
seen_ids = set()
|
|
138
|
+
for position, (start, item_id, name) in enumerate(item_starts):
|
|
139
|
+
end = item_starts[position + 1][0] if position + 1 < len(item_starts) else len(lines)
|
|
140
|
+
fields, field_errors = _parse_fields(lines[start + 1:end], f"穿刺项 {item_id}")
|
|
141
|
+
errors.extend(field_errors)
|
|
142
|
+
if item_id in seen_ids:
|
|
143
|
+
errors.append(f"穿刺清单重复编号:{item_id}")
|
|
144
|
+
seen_ids.add(item_id)
|
|
145
|
+
errors.extend(_require_fields(fields, INDEX_REQUIRED_FIELDS, f"穿刺项 {item_id}"))
|
|
146
|
+
items.append(SpikeIndexItem(item_id=item_id, name=name, fields=fields))
|
|
147
|
+
|
|
148
|
+
if not items:
|
|
149
|
+
errors.append("穿刺清单中没有任何穿刺项")
|
|
150
|
+
return workflow_id, items, errors
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def parse_spike_detail(path: str) -> tuple[SpikeDetail, list[str]]:
|
|
154
|
+
lines = _read_text(path).splitlines()
|
|
155
|
+
section_starts = []
|
|
156
|
+
for index, line in enumerate(lines):
|
|
157
|
+
match = DETAIL_SECTION_RE.match(line.strip())
|
|
158
|
+
if match:
|
|
159
|
+
section_starts.append((index, match.group(1), match.group(2).strip()))
|
|
160
|
+
|
|
161
|
+
errors = []
|
|
162
|
+
top_end = section_starts[0][0] if section_starts else len(lines)
|
|
163
|
+
metadata, metadata_errors = _parse_fields(lines[:top_end], os.path.basename(path))
|
|
164
|
+
errors.extend(metadata_errors)
|
|
165
|
+
errors.extend(_require_fields(metadata, ["工作流编号", "穿刺项编号"], os.path.basename(path)))
|
|
166
|
+
|
|
167
|
+
sections: dict[str, str] = {}
|
|
168
|
+
section_fields: dict[str, dict[str, str]] = {}
|
|
169
|
+
for position, (start, number, title) in enumerate(section_starts):
|
|
170
|
+
end = section_starts[position + 1][0] if position + 1 < len(section_starts) else len(lines)
|
|
171
|
+
if number in sections:
|
|
172
|
+
errors.append(f"{os.path.basename(path)} 重复章节:{number}")
|
|
173
|
+
continue
|
|
174
|
+
expected_title = EXPECTED_DETAIL_SECTIONS.get(number)
|
|
175
|
+
if expected_title != title:
|
|
176
|
+
errors.append(
|
|
177
|
+
f"{os.path.basename(path)} 第 {number} 章标题应为“{expected_title}”,实际为“{title}”"
|
|
178
|
+
)
|
|
179
|
+
body_lines = lines[start + 1:end]
|
|
180
|
+
body = "\n".join(body_lines).strip()
|
|
181
|
+
sections[number] = body
|
|
182
|
+
fields, field_errors = _parse_fields(body_lines, f"{os.path.basename(path)} 第 {number} 章")
|
|
183
|
+
section_fields[number] = fields
|
|
184
|
+
errors.extend(field_errors)
|
|
185
|
+
|
|
186
|
+
for number, title in EXPECTED_DETAIL_SECTIONS.items():
|
|
187
|
+
if number not in sections:
|
|
188
|
+
errors.append(f"{os.path.basename(path)} 缺少第 {number} 章“{title}”")
|
|
189
|
+
continue
|
|
190
|
+
body = sections[number]
|
|
191
|
+
if not body or PLACEHOLDER_RE.search(body):
|
|
192
|
+
errors.append(f"{os.path.basename(path)} 第 {number} 章未填写完成")
|
|
193
|
+
|
|
194
|
+
errors.extend(_require_fields(section_fields.get("4", {}), METHOD_FIELDS, f"{os.path.basename(path)} 第 4 章"))
|
|
195
|
+
errors.extend(_require_fields(section_fields.get("5", {}), EXECUTION_FIELDS, f"{os.path.basename(path)} 第 5 章"))
|
|
196
|
+
errors.extend(_require_fields(section_fields.get("7", {}), RESULT_FIELDS, f"{os.path.basename(path)} 第 7 章"))
|
|
197
|
+
errors.extend(_require_fields(section_fields.get("8", {}), IMPACT_FIELDS, f"{os.path.basename(path)} 第 8 章"))
|
|
198
|
+
|
|
199
|
+
observation = sections.get("6", "").strip()
|
|
200
|
+
if observation in {"无", "暂无"}:
|
|
201
|
+
errors.append(f"{os.path.basename(path)} 第 6 章必须记录实际观察结果")
|
|
202
|
+
|
|
203
|
+
return SpikeDetail(metadata=metadata, sections=sections, section_fields=section_fields), errors
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _detail_path_from_link(project_root: str, value: str) -> tuple[str | None, str | None]:
|
|
207
|
+
match = MARKDOWN_LINK_RE.search(value)
|
|
208
|
+
if not match:
|
|
209
|
+
return (None, "结论文档字段必须使用 Markdown 链接")
|
|
210
|
+
target = match.group(1).split("#", 1)[0].strip()
|
|
211
|
+
if os.path.isabs(target):
|
|
212
|
+
return (None, "结论文档不能使用绝对路径")
|
|
213
|
+
|
|
214
|
+
if target.startswith("./"):
|
|
215
|
+
target = target[2:]
|
|
216
|
+
if target.startswith("spec/"):
|
|
217
|
+
rel_path = os.path.normpath(target)
|
|
218
|
+
else:
|
|
219
|
+
rel_path = os.path.normpath(os.path.join("spec", target))
|
|
220
|
+
|
|
221
|
+
if rel_path.startswith(".."):
|
|
222
|
+
return (None, "结论文档路径不能离开 spec/ 目录")
|
|
223
|
+
# 结论文档使用稳定中文文件标识:spec/穿刺_<穿刺项文件标识>.md
|
|
224
|
+
normalized = rel_path.replace(os.sep, "/")
|
|
225
|
+
if not re.fullmatch(r"spec/穿刺_[A-Za-z0-9_\-一-鿿㐀-䶿]+\.md", normalized):
|
|
226
|
+
return (None, "结论文档文件名必须是 spec/穿刺_<穿刺项文件标识>.md")
|
|
227
|
+
if normalized == artifact_paths_mod.SPIKE_INDEX_DOC:
|
|
228
|
+
return (None, "穿刺清单不能作为结论文档")
|
|
229
|
+
return (rel_path, None)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _validate_choice(value: str | None, allowed: set[str], label: str, context: str) -> list[str]:
|
|
233
|
+
if value not in allowed:
|
|
234
|
+
return [f"{context} 的“{label}”值无效:{value or '空'}"]
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _is_none_value(value: str | None) -> bool:
|
|
239
|
+
return value is None or not value.strip() or value.strip() in {"无", "暂无"}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def validate_spike_stage(project_root: str) -> tuple[bool, str]:
|
|
243
|
+
state = state_mod.load_state(project_root)
|
|
244
|
+
if state is None:
|
|
245
|
+
return (False, "找不到当前工作流状态")
|
|
246
|
+
|
|
247
|
+
index_path = os.path.join(project_root, artifact_paths_mod.SPIKE_INDEX_DOC)
|
|
248
|
+
if not os.path.exists(index_path):
|
|
249
|
+
return (False, f"{artifact_paths_mod.SPIKE_INDEX_DOC} 不存在")
|
|
250
|
+
|
|
251
|
+
workflow_id, items, errors = parse_spike_index(index_path)
|
|
252
|
+
if workflow_id and workflow_id != state.workflow_id:
|
|
253
|
+
errors.append(
|
|
254
|
+
f"穿刺清单属于工作流 {workflow_id},当前工作流是 {state.workflow_id}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
seen_detail_paths = set()
|
|
258
|
+
product_design_needs_change = False
|
|
259
|
+
code_design_needs_change = False
|
|
260
|
+
|
|
261
|
+
for item in items:
|
|
262
|
+
context = f"穿刺项 {item.item_id}"
|
|
263
|
+
fields = item.fields
|
|
264
|
+
if any(key not in fields or _missing_or_placeholder(fields.get(key)) for key in INDEX_REQUIRED_FIELDS):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
errors.extend(_validate_choice(fields.get("穿刺状态"), INDEX_STATUSES, "穿刺状态", context))
|
|
268
|
+
if fields.get("穿刺状态") == "待验证":
|
|
269
|
+
errors.append(f"{context} 仍是待验证,不能通过门2")
|
|
270
|
+
errors.extend(_validate_choice(fields.get("是否阻塞后续"), YES_NO, "是否阻塞后续", context))
|
|
271
|
+
errors.extend(_validate_choice(fields.get("产品设计影响"), DESIGN_IMPACTS, "产品设计影响", context))
|
|
272
|
+
errors.extend(_validate_choice(fields.get("代码设计影响"), DESIGN_IMPACTS, "代码设计影响", context))
|
|
273
|
+
errors.extend(_validate_choice(fields.get("后续处理阶段"), FOLLOW_UP_STAGES, "后续处理阶段", context))
|
|
274
|
+
|
|
275
|
+
detail_rel_path, link_error = _detail_path_from_link(project_root, fields["结论文档"])
|
|
276
|
+
if link_error:
|
|
277
|
+
errors.append(f"{context}:{link_error}")
|
|
278
|
+
continue
|
|
279
|
+
assert detail_rel_path is not None
|
|
280
|
+
if detail_rel_path in seen_detail_paths:
|
|
281
|
+
errors.append(f"多个穿刺项使用同一份结论文档:{detail_rel_path}")
|
|
282
|
+
continue
|
|
283
|
+
seen_detail_paths.add(detail_rel_path)
|
|
284
|
+
|
|
285
|
+
detail_path = os.path.join(project_root, detail_rel_path)
|
|
286
|
+
if not os.path.exists(detail_path):
|
|
287
|
+
errors.append(f"{context} 的结论文档不存在:{detail_rel_path}")
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
detail, detail_errors = parse_spike_detail(detail_path)
|
|
291
|
+
errors.extend(detail_errors)
|
|
292
|
+
if detail.metadata.get("工作流编号") != state.workflow_id:
|
|
293
|
+
errors.append(f"{detail_rel_path} 的工作流编号与当前工作流不一致")
|
|
294
|
+
if detail.metadata.get("穿刺项编号") != item.item_id:
|
|
295
|
+
errors.append(f"{detail_rel_path} 的穿刺项编号与清单中的 {item.item_id} 不一致")
|
|
296
|
+
|
|
297
|
+
result_fields = detail.section_fields.get("7", {})
|
|
298
|
+
impact_fields = detail.section_fields.get("8", {})
|
|
299
|
+
result_status = result_fields.get("结果状态")
|
|
300
|
+
blocked = result_fields.get("是否阻塞后续")
|
|
301
|
+
product_impact = impact_fields.get("产品设计影响")
|
|
302
|
+
code_impact = impact_fields.get("代码设计影响")
|
|
303
|
+
follow_up = impact_fields.get("后续处理阶段")
|
|
304
|
+
|
|
305
|
+
errors.extend(_validate_choice(result_status, RESULT_STATUSES, "结果状态", detail_rel_path))
|
|
306
|
+
errors.extend(_validate_choice(blocked, YES_NO, "是否阻塞后续", detail_rel_path))
|
|
307
|
+
errors.extend(_validate_choice(product_impact, DESIGN_IMPACTS, "产品设计影响", detail_rel_path))
|
|
308
|
+
errors.extend(_validate_choice(code_impact, DESIGN_IMPACTS, "代码设计影响", detail_rel_path))
|
|
309
|
+
errors.extend(_validate_choice(follow_up, FOLLOW_UP_STAGES, "后续处理阶段", detail_rel_path))
|
|
310
|
+
|
|
311
|
+
if fields.get("穿刺状态") != result_status:
|
|
312
|
+
errors.append(f"{context} 的穿刺状态与结论文档结果状态不一致")
|
|
313
|
+
if fields.get("是否阻塞后续") != blocked:
|
|
314
|
+
errors.append(f"{context} 的阻塞状态与结论文档不一致")
|
|
315
|
+
if fields.get("产品设计影响") != product_impact:
|
|
316
|
+
errors.append(f"{context} 的产品设计影响与结论文档不一致")
|
|
317
|
+
if fields.get("代码设计影响") != code_impact:
|
|
318
|
+
errors.append(f"{context} 的代码设计影响与结论文档不一致")
|
|
319
|
+
if fields.get("后续处理阶段") != follow_up:
|
|
320
|
+
errors.append(f"{context} 的后续处理阶段与结论文档不一致")
|
|
321
|
+
|
|
322
|
+
if blocked == "是":
|
|
323
|
+
errors.append(f"{context} 仍然阻塞后续,不能通过门2")
|
|
324
|
+
|
|
325
|
+
if result_status == "仍未确认":
|
|
326
|
+
if _is_none_value(result_fields.get("仍未确认内容")):
|
|
327
|
+
errors.append(f"{detail_rel_path} 必须写清仍未确认内容")
|
|
328
|
+
if _is_none_value(impact_fields.get("剩余风险")):
|
|
329
|
+
errors.append(f"{detail_rel_path} 必须写清剩余风险")
|
|
330
|
+
if follow_up == "无":
|
|
331
|
+
errors.append(f"{detail_rel_path} 必须指定后续处理阶段")
|
|
332
|
+
if _is_none_value(impact_fields.get("后续需要检查什么")):
|
|
333
|
+
errors.append(f"{detail_rel_path} 必须写清后续需要检查什么")
|
|
334
|
+
|
|
335
|
+
if state.intent == "bugfix" and product_impact == "需要修改":
|
|
336
|
+
errors.append(
|
|
337
|
+
f"{detail_rel_path} 需要修改产品设计,不能继续 bugfix;请结束当前流程后启动 product_change"
|
|
338
|
+
)
|
|
339
|
+
product_location = impact_fields.get("产品设计更新位置")
|
|
340
|
+
code_location = impact_fields.get("代码设计更新位置")
|
|
341
|
+
if product_impact == "需要修改":
|
|
342
|
+
product_design_needs_change = True
|
|
343
|
+
if _is_none_value(product_location):
|
|
344
|
+
errors.append(f"{detail_rel_path} 必须写清产品设计更新位置")
|
|
345
|
+
elif not _is_none_value(product_location):
|
|
346
|
+
errors.append(f"{detail_rel_path} 产品设计无需修改时,更新位置应写“无”")
|
|
347
|
+
|
|
348
|
+
if code_impact == "需要修改":
|
|
349
|
+
code_design_needs_change = True
|
|
350
|
+
if _is_none_value(code_location):
|
|
351
|
+
errors.append(f"{detail_rel_path} 必须写清代码设计更新位置")
|
|
352
|
+
elif not _is_none_value(code_location):
|
|
353
|
+
errors.append(f"{detail_rel_path} 代码设计无需修改时,更新位置应写“无”")
|
|
354
|
+
|
|
355
|
+
baseline = state.spike_baseline
|
|
356
|
+
if baseline.captured_at is None:
|
|
357
|
+
if not baseline.legacy_unavailable:
|
|
358
|
+
errors.append("穿刺开始时的产品设计和代码设计基线尚未记录")
|
|
359
|
+
elif product_design_needs_change or code_design_needs_change:
|
|
360
|
+
errors.append(
|
|
361
|
+
"当前是缺少入场设计基线的旧工作流,无法证明设计文档在穿刺后发生了变化"
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
current_product_hash, _ = verification_mod.compute_product_design_hash(project_root)
|
|
365
|
+
current_code_hash = verification_mod.compute_code_design_hash(project_root)
|
|
366
|
+
if product_design_needs_change:
|
|
367
|
+
if baseline.product_design_hash is None or current_product_hash is None:
|
|
368
|
+
errors.append("无法比较产品设计修改前后的哈希")
|
|
369
|
+
elif baseline.product_design_hash == current_product_hash:
|
|
370
|
+
errors.append("穿刺结论要求修改产品设计,但产品设计哈希没有变化")
|
|
371
|
+
if code_design_needs_change:
|
|
372
|
+
if baseline.code_design_hash is None or current_code_hash is None:
|
|
373
|
+
errors.append("无法比较代码设计修改前后的哈希")
|
|
374
|
+
elif baseline.code_design_hash == current_code_hash:
|
|
375
|
+
errors.append("穿刺结论要求修改代码设计,但代码设计哈希没有变化")
|
|
376
|
+
|
|
377
|
+
if errors:
|
|
378
|
+
return (False, ";".join(errors))
|
|
379
|
+
return (True, f"穿刺清单和 {len(items)} 份结论文档通过校验")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""阶段材料清单:检查必读文件、生成有序清单和内容指纹。
|
|
2
|
+
|
|
3
|
+
材料的消费者是 AI:`workflow discuss` 只输出经过存在性和可读性检查的绝对路径、
|
|
4
|
+
用途和读取顺序,不在 stdout 重复正文。程序保存内容指纹,用于发现讨论后材料
|
|
5
|
+
又被修改的情况;指纹和清单记录都只证明程序给出过当时有效的完整清单,
|
|
6
|
+
不能冒充 AI 已经阅读或理解的证明。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
# .workflow_loop 目录名(材料相对路径都以它为根)
|
|
16
|
+
WORKFLOW_LOOP_DIRNAME = ".workflow_loop"
|
|
17
|
+
|
|
18
|
+
# 所有阶段共同的全局必读材料,按顺序排在阶段材料之前。
|
|
19
|
+
# 每项是 (相对 .workflow_loop/ 的路径, 用途说明)。
|
|
20
|
+
GLOBAL_MATERIALS: list[tuple[str, str]] = [
|
|
21
|
+
(
|
|
22
|
+
"Standardized_Repository/global/document_writing.md",
|
|
23
|
+
"全局写作规范:所有阶段的正式文档和回复共同遵守的表达规则",
|
|
24
|
+
),
|
|
25
|
+
(
|
|
26
|
+
"Standardized_Repository/global/workflow_lifecycle.md",
|
|
27
|
+
"全局工作流生命周期规范:返回、作废与收工的统一规则",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MaterialError(ValueError):
|
|
33
|
+
"""材料缺失、不是普通文件或不可读时抛出;调用方不得登记本次清单。"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class StageMaterial:
|
|
38
|
+
"""清单中的一份必读材料。"""
|
|
39
|
+
|
|
40
|
+
order: int
|
|
41
|
+
# 相对 .workflow_loop/ 的路径;None 表示该项没有对应文件(明示"无")
|
|
42
|
+
relative_path: str | None
|
|
43
|
+
# 当前操作系统的原生绝对路径;None 表示没有文件
|
|
44
|
+
absolute_path: str | None
|
|
45
|
+
# 这份文件的用途
|
|
46
|
+
purpose: str
|
|
47
|
+
# 没有文件时的明示说明
|
|
48
|
+
note: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class MaterialChecklist:
|
|
53
|
+
"""一个阶段的完整材料清单。"""
|
|
54
|
+
|
|
55
|
+
stage_name: str
|
|
56
|
+
role_title: str
|
|
57
|
+
role_description: str
|
|
58
|
+
# 程序按当前状态生成的短阶段任务说明
|
|
59
|
+
task_text: str
|
|
60
|
+
# 按读取顺序排列的真实必读文件
|
|
61
|
+
materials: list[StageMaterial] = field(default_factory=list)
|
|
62
|
+
# 明示"无"的占位项(该阶段没有对应模板或规范)
|
|
63
|
+
placeholders: list[StageMaterial] = field(default_factory=list)
|
|
64
|
+
# 清单内容指纹:覆盖角色说明、阶段任务、全部材料路径与正文
|
|
65
|
+
fingerprint: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_material_file(absolute_path: str, relative_path: str) -> str:
|
|
69
|
+
"""检查真实存在、是普通文件且可读;返回文件内容。"""
|
|
70
|
+
if not os.path.exists(absolute_path):
|
|
71
|
+
raise MaterialError(f"必读材料不存在: {relative_path}({absolute_path})")
|
|
72
|
+
if not os.path.isfile(absolute_path):
|
|
73
|
+
raise MaterialError(f"必读材料不是普通文件: {relative_path}({absolute_path})")
|
|
74
|
+
try:
|
|
75
|
+
with open(absolute_path, "r", encoding="utf-8") as stream:
|
|
76
|
+
return stream.read()
|
|
77
|
+
except OSError as exc:
|
|
78
|
+
raise MaterialError(f"必读材料无法读取: {relative_path}({exc})") from exc
|
|
79
|
+
except UnicodeDecodeError as exc:
|
|
80
|
+
raise MaterialError(f"必读材料不是可读文本: {relative_path}({exc})") from exc
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_checklist(
|
|
84
|
+
project_root: str,
|
|
85
|
+
stage_name: str,
|
|
86
|
+
role_doc: dict | None,
|
|
87
|
+
task_text: str,
|
|
88
|
+
stage_specs: list,
|
|
89
|
+
) -> MaterialChecklist:
|
|
90
|
+
"""组装并校验当前阶段的材料清单,计算内容指纹。
|
|
91
|
+
|
|
92
|
+
stage_specs 是阶段策略 `materials()` 返回的 MaterialSpec 列表
|
|
93
|
+
(relative_path 为 None 表示该阶段没有对应材料,明示"无")。
|
|
94
|
+
任一文件不满足存在、普通文件、可读时抛 MaterialError,调用方不得登记清单。
|
|
95
|
+
"""
|
|
96
|
+
role_title = (role_doc or {}).get("role", "")
|
|
97
|
+
role_description = (role_doc or {}).get("description", "")
|
|
98
|
+
|
|
99
|
+
checklist = MaterialChecklist(
|
|
100
|
+
stage_name=stage_name,
|
|
101
|
+
role_title=role_title,
|
|
102
|
+
role_description=role_description,
|
|
103
|
+
task_text=task_text,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# 指纹覆盖:阶段名、角色说明、内置阶段任务、全部材料的路径与正文、占位项
|
|
107
|
+
digest = hashlib.sha256()
|
|
108
|
+
digest.update(f"stage:{stage_name}\n".encode("utf-8"))
|
|
109
|
+
digest.update(f"role:{role_title}\n{role_description}\n".encode("utf-8"))
|
|
110
|
+
digest.update(f"task:{task_text}\n".encode("utf-8"))
|
|
111
|
+
|
|
112
|
+
order = 0
|
|
113
|
+
all_specs = [
|
|
114
|
+
(relative_path, purpose, "")
|
|
115
|
+
for relative_path, purpose in GLOBAL_MATERIALS
|
|
116
|
+
] + [
|
|
117
|
+
(spec.relative_path, spec.purpose, spec.missing_note)
|
|
118
|
+
for spec in stage_specs
|
|
119
|
+
]
|
|
120
|
+
for relative_path, purpose, missing_note in all_specs:
|
|
121
|
+
if relative_path is None:
|
|
122
|
+
checklist.placeholders.append(
|
|
123
|
+
StageMaterial(
|
|
124
|
+
order=-1,
|
|
125
|
+
relative_path=None,
|
|
126
|
+
absolute_path=None,
|
|
127
|
+
purpose=purpose,
|
|
128
|
+
note=missing_note or "无",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
digest.update(f"none:{purpose}:{missing_note}\n".encode("utf-8"))
|
|
132
|
+
continue
|
|
133
|
+
order += 1
|
|
134
|
+
# 绝对路径使用当前操作系统的原生格式,可直接交给文件读取工具
|
|
135
|
+
absolute_path = os.path.abspath(
|
|
136
|
+
os.path.join(project_root, WORKFLOW_LOOP_DIRNAME, relative_path)
|
|
137
|
+
)
|
|
138
|
+
content = _validate_material_file(absolute_path, relative_path)
|
|
139
|
+
checklist.materials.append(
|
|
140
|
+
StageMaterial(
|
|
141
|
+
order=order,
|
|
142
|
+
relative_path=relative_path,
|
|
143
|
+
absolute_path=absolute_path,
|
|
144
|
+
purpose=purpose,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
digest.update(f"file:{relative_path}\n".encode("utf-8"))
|
|
148
|
+
digest.update(content.encode("utf-8"))
|
|
149
|
+
digest.update(b"\n")
|
|
150
|
+
|
|
151
|
+
checklist.fingerprint = digest.hexdigest()
|
|
152
|
+
return checklist
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def compute_fingerprint(
|
|
156
|
+
project_root: str,
|
|
157
|
+
stage_name: str,
|
|
158
|
+
role_doc: dict | None,
|
|
159
|
+
task_text: str,
|
|
160
|
+
stage_specs: list,
|
|
161
|
+
) -> str:
|
|
162
|
+
"""只计算当前材料指纹;文件缺失或不可读时同样抛 MaterialError。"""
|
|
163
|
+
return build_checklist(
|
|
164
|
+
project_root,
|
|
165
|
+
stage_name,
|
|
166
|
+
role_doc,
|
|
167
|
+
task_text,
|
|
168
|
+
stage_specs,
|
|
169
|
+
).fingerprint
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 导入阶段基类与公共工具,供所有阶段子类继承和复用
|
|
2
|
+
from .base import StageStrategy, clean_spike_tmp
|
|
3
|
+
# 导入工作流中各阶段策略类
|
|
4
|
+
from .stages import (
|
|
5
|
+
SpecStage,
|
|
6
|
+
CodeDesignStage,
|
|
7
|
+
SpikeStage,
|
|
8
|
+
AcceptancePlanStage,
|
|
9
|
+
TestPlanStage,
|
|
10
|
+
ImplStage,
|
|
11
|
+
TestCodeStage,
|
|
12
|
+
TestExecutionStage,
|
|
13
|
+
TopicAcceptanceStage,
|
|
14
|
+
TestStage,
|
|
15
|
+
AcceptanceStage,
|
|
16
|
+
RegressionTestStage,
|
|
17
|
+
OverallAcceptanceStage,
|
|
18
|
+
UpdateCodeDesignStage,
|
|
19
|
+
ProjectDesignInitStage,
|
|
20
|
+
ReviseCodeDesignStage,
|
|
21
|
+
ReproduceStage,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# 对外公开的阶段 API 清单,约束 from workflow_loop.stages import * 的导出范围,避免内部实现泄露
|
|
25
|
+
__all__ = [
|
|
26
|
+
"StageStrategy",
|
|
27
|
+
"clean_spike_tmp",
|
|
28
|
+
"SpecStage",
|
|
29
|
+
"CodeDesignStage",
|
|
30
|
+
"SpikeStage",
|
|
31
|
+
"AcceptancePlanStage",
|
|
32
|
+
"TestPlanStage",
|
|
33
|
+
"ImplStage",
|
|
34
|
+
"TestCodeStage",
|
|
35
|
+
"TestExecutionStage",
|
|
36
|
+
"TopicAcceptanceStage",
|
|
37
|
+
"TestStage",
|
|
38
|
+
"AcceptanceStage",
|
|
39
|
+
"RegressionTestStage",
|
|
40
|
+
"OverallAcceptanceStage",
|
|
41
|
+
"UpdateCodeDesignStage",
|
|
42
|
+
"ProjectDesignInitStage",
|
|
43
|
+
"ReviseCodeDesignStage",
|
|
44
|
+
"ReproduceStage",
|
|
45
|
+
]
|