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
workflow_loop/topic.py ADDED
@@ -0,0 +1,114 @@
1
+ import os
2
+ import re
3
+
4
+ from . import artifact_paths as artifact_paths_mod
5
+ from .project import load_project
6
+ from .state import load_state
7
+ from .topic_relations import read_topic_index
8
+
9
+
10
+ def topic_file_key(project_root: str, topic: str) -> str:
11
+ """取得验收主题的稳定文件标识:优先项目映射,缺少时确定性生成(不保存)。
12
+
13
+ 主题名称是业务标识;文件标识只用于拼路径。文件名清理不改变主题显示名称。
14
+ """
15
+ project = load_project(project_root)
16
+ return artifact_paths_mod.resolve_key_for(project, "topic", topic)
17
+
18
+
19
+ def topic_paths(project_root: str, topic: str) -> dict[str, str]:
20
+ """按统一路径规则返回一个主题的全部正式文档路径(相对项目根)。"""
21
+ file_key = topic_file_key(project_root, topic)
22
+ return {
23
+ "acceptance_plan": artifact_paths_mod.topic_acceptance_plan(file_key),
24
+ "acceptance_result": artifact_paths_mod.topic_acceptance_result(file_key),
25
+ "test_plan": artifact_paths_mod.topic_test_plan(file_key),
26
+ "test_result": artifact_paths_mod.topic_test_result(file_key),
27
+ "impl_doc": artifact_paths_mod.topic_impl_doc(file_key),
28
+ }
29
+
30
+
31
+ def list_acceptance_index_topics(
32
+ project_root: str,
33
+ workflow_id: str | None = None,
34
+ ) -> list[str]:
35
+ """按 acceptance/索引.md 的展示顺序读取验收主题完整显示名称。"""
36
+
37
+ state = load_state(project_root)
38
+ effective_workflow_id = workflow_id or (state.workflow_id if state is not None else None)
39
+ if effective_workflow_id is None:
40
+ return []
41
+ try:
42
+ relations = read_topic_index(
43
+ project_root,
44
+ artifact_paths_mod.ACCEPTANCE_INDEX_DOC,
45
+ effective_workflow_id,
46
+ )
47
+ except ValueError:
48
+ return []
49
+ return [relation.topic for relation in relations]
50
+
51
+
52
+ def list_reproduce_topics(project_root: str, workflow_id: str | None = None) -> list[str]:
53
+ """从当前工作流的缺陷复现记录读取验收主题。"""
54
+ bug_dir = os.path.join(project_root, "bug")
55
+ if not os.path.isdir(bug_dir):
56
+ return []
57
+
58
+ topics: list[str] = []
59
+ for filename in sorted(os.listdir(bug_dir)):
60
+ if (
61
+ not filename.endswith(".md")
62
+ or filename == os.path.basename(artifact_paths_mod.BUG_INDEX_DOC)
63
+ ):
64
+ continue
65
+ with open(os.path.join(bug_dir, filename), "r", encoding="utf-8") as f:
66
+ content = f.read()
67
+ if workflow_id is not None:
68
+ workflow_match = re.search(r"^-\s*工作流编号:\s*(.+?)\s*$", content, re.MULTILINE)
69
+ if workflow_match is None or workflow_match.group(1).strip() != workflow_id:
70
+ continue
71
+ topic_match = re.search(r"^-\s*验收主题:\s*(.+?)\s*$", content, re.MULTILINE)
72
+ if topic_match is not None:
73
+ topics.append(topic_match.group(1).strip())
74
+ return topics
75
+
76
+
77
+ def current_workflow_topics(project_root: str) -> list[str]:
78
+ """读取当前 Workflow Run(工作流运行)的主题,兼容旧版单主题状态。"""
79
+ state = load_state(project_root)
80
+ if state is None:
81
+ return []
82
+ if state.topics:
83
+ return state.topics
84
+ return [state.topic] if state.topic else []
85
+
86
+
87
+ def candidate_topics(project_root: str) -> list[str]:
88
+ """返回本次验收计划里的主题:保留当前主题,并接纳未使用过的新主题。"""
89
+ current = set(current_workflow_topics(project_root))
90
+ project = load_project(project_root)
91
+ history = set(project.topic_history if project is not None else [])
92
+ topics = list_acceptance_index_topics(project_root)
93
+ return [
94
+ topic
95
+ for topic in topics
96
+ if topic in current or topic not in history
97
+ ]
98
+
99
+
100
+ def missing_topic_documents(
101
+ project_root: str,
102
+ kind: str,
103
+ topics: list[str],
104
+ ) -> list[str]:
105
+ """返回缺失的主题正式文档路径。
106
+
107
+ kind 取值:acceptance_plan / acceptance_result / test_plan / test_result / impl_doc。
108
+ """
109
+ missing: list[str] = []
110
+ for topic in topics:
111
+ relative_path = topic_paths(project_root, topic)[kind]
112
+ if not os.path.isfile(os.path.join(project_root, relative_path)):
113
+ missing.append(relative_path)
114
+ return missing
@@ -0,0 +1,202 @@
1
+ """读取各阶段索引中的验收主题关系。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import os
7
+ import re
8
+
9
+ from . import artifact_paths as artifact_paths_mod
10
+ from .project import load_project
11
+
12
+
13
+ BASE_INDEX_HEADERS = ["展示顺序", "验收主题", "前置主题"]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class TopicRelation:
18
+ """一个验收主题及其前置主题。"""
19
+
20
+ order: int
21
+ topic: str
22
+ prerequisites: tuple[str, ...]
23
+ links: dict[str, str]
24
+
25
+
26
+ def _workflow_section(content: str, workflow_id: str) -> str:
27
+ match = re.search(
28
+ rf"^##\s+{re.escape(workflow_id)}\s*$\n(.*?)(?=^##\s+|\Z)",
29
+ content,
30
+ re.MULTILINE | re.DOTALL,
31
+ )
32
+ if match is None:
33
+ raise ValueError(f"索引缺少当前工作流章节: {workflow_id}")
34
+ return match.group(1)
35
+
36
+
37
+ def _table_cells(line: str) -> list[str] | None:
38
+ stripped = line.strip()
39
+ if not stripped.startswith("|") or not stripped.endswith("|"):
40
+ return None
41
+ cells = [cell.strip() for cell in stripped.strip("|").split("|")]
42
+ if not cells or all(re.fullmatch(r"[-:]+", cell) for cell in cells):
43
+ return None
44
+ return cells
45
+
46
+
47
+ def _link_path(cell: str, allowed_text: set[str] | None = None) -> str:
48
+ if allowed_text is not None and cell.strip() in allowed_text:
49
+ return cell.strip()
50
+ match = re.fullmatch(r"\[[^\]]+\]\(([^)]+)\)", cell.strip())
51
+ if match is None:
52
+ raise ValueError(f"索引单元格不是单一 Markdown 链接: {cell}")
53
+ return match.group(1).strip()
54
+
55
+
56
+ def _prerequisites(cell: str) -> tuple[str, ...]:
57
+ value = cell.strip()
58
+ if value == "无":
59
+ return ()
60
+ topics = tuple(part.strip() for part in re.split(r"[、,,]", value) if part.strip())
61
+ if not topics:
62
+ raise ValueError("前置主题不能为空;没有依赖时写“无”")
63
+ return topics
64
+
65
+
66
+ def read_topic_index(
67
+ project_root: str,
68
+ relative_path: str,
69
+ workflow_id: str,
70
+ expected_headers: list[str] | None = None,
71
+ allowed_text_values: dict[str, set[str]] | None = None,
72
+ ) -> list[TopicRelation]:
73
+ """读取一个主题索引,返回按展示顺序排列的关系。"""
74
+
75
+ full_path = os.path.join(project_root, relative_path)
76
+ if not os.path.isfile(full_path):
77
+ raise ValueError(f"{relative_path} 不存在")
78
+ with open(full_path, "r", encoding="utf-8") as stream:
79
+ section = _workflow_section(stream.read(), workflow_id)
80
+
81
+ lines = section.splitlines()
82
+ header_index = next(
83
+ (
84
+ index
85
+ for index, line in enumerate(lines)
86
+ if _has_index_headers(_table_cells(line), expected_headers)
87
+ ),
88
+ None,
89
+ )
90
+ if header_index is None:
91
+ raise ValueError(f"{relative_path} 缺少主题关系表")
92
+
93
+ relations: list[TopicRelation] = []
94
+ for line in lines[header_index + 1 :]:
95
+ cells = _table_cells(line)
96
+ if cells is None:
97
+ continue
98
+ headers = _table_cells(lines[header_index])
99
+ if headers is None or len(cells) != len(headers):
100
+ raise ValueError(f"{relative_path} 主题关系表列数与表头不一致")
101
+ try:
102
+ order = int(cells[0])
103
+ except ValueError as exc:
104
+ raise ValueError(f"{relative_path} 展示顺序必须是整数: {cells[0]}") from exc
105
+
106
+ allowed_values = allowed_text_values or {}
107
+ link_cells = {
108
+ header: _link_path(cells[index], allowed_values.get(header))
109
+ for index, header in enumerate(headers[3:], start=3)
110
+ }
111
+ plan_path = link_cells.get("验收计划")
112
+ if plan_path is None:
113
+ raise ValueError(f"{relative_path} 缺少验收计划链接列")
114
+ # 验收主题以显示名称列为准;文件标识只用于核对链接路径,
115
+ # 不再从文件名反推业务名称(显示名称与文件标识分离)。
116
+ topic = cells[1].strip()
117
+ if not topic:
118
+ raise ValueError(f"{relative_path} 验收主题不能为空")
119
+ project = load_project(project_root)
120
+ expected_key = artifact_paths_mod.resolve_key_for(project, "topic", topic)
121
+ expected_plan_name = os.path.basename(
122
+ artifact_paths_mod.topic_acceptance_plan(expected_key)
123
+ )
124
+ if os.path.basename(plan_path) != expected_plan_name:
125
+ raise ValueError(
126
+ f"{relative_path} 主题“{topic}”的验收计划链接应指向 {expected_plan_name},"
127
+ f"实际是 {plan_path}"
128
+ )
129
+ relations.append(
130
+ TopicRelation(
131
+ order=order,
132
+ topic=topic,
133
+ prerequisites=_prerequisites(cells[2]),
134
+ links=link_cells,
135
+ )
136
+ )
137
+
138
+ if not relations:
139
+ raise ValueError(f"{relative_path} 主题关系表没有数据行")
140
+
141
+ topics = [relation.topic for relation in relations]
142
+ if len(topics) != len(set(topics)):
143
+ raise ValueError(f"{relative_path} 存在重复验收主题")
144
+ orders = [relation.order for relation in relations]
145
+ if len(orders) != len(set(orders)):
146
+ raise ValueError(f"{relative_path} 展示顺序不能重复")
147
+
148
+ known = set(topics)
149
+ order_by_topic = {relation.topic: relation.order for relation in relations}
150
+ dependencies = {relation.topic: relation.prerequisites for relation in relations}
151
+ for relation in relations:
152
+ for prerequisite in relation.prerequisites:
153
+ if prerequisite not in known:
154
+ raise ValueError(
155
+ f"{relative_path} 主题“{relation.topic}”引用了不存在的前置主题“{prerequisite}”"
156
+ )
157
+ if prerequisite == relation.topic:
158
+ raise ValueError(f"{relative_path} 主题“{relation.topic}”不能依赖自己")
159
+ if order_by_topic[prerequisite] >= relation.order:
160
+ raise ValueError(
161
+ f"{relative_path} 主题“{relation.topic}”的前置主题“{prerequisite}”必须排在前面"
162
+ )
163
+
164
+ visiting: set[str] = set()
165
+ visited: set[str] = set()
166
+
167
+ def visit(topic: str) -> None:
168
+ if topic in visiting:
169
+ raise ValueError(f"{relative_path} 的主题前置关系存在循环")
170
+ if topic in visited:
171
+ return
172
+ visiting.add(topic)
173
+ for prerequisite in dependencies[topic]:
174
+ visit(prerequisite)
175
+ visiting.remove(topic)
176
+ visited.add(topic)
177
+
178
+ for topic in topics:
179
+ visit(topic)
180
+ return relations
181
+
182
+
183
+ def _has_index_headers(
184
+ cells: list[str] | None,
185
+ expected_headers: list[str] | None,
186
+ ) -> bool:
187
+ if cells is None or len(cells) < len(BASE_INDEX_HEADERS) + 2:
188
+ return False
189
+ if cells[: len(BASE_INDEX_HEADERS)] != BASE_INDEX_HEADERS:
190
+ return False
191
+ return expected_headers is None or cells == expected_headers
192
+
193
+
194
+ def relation_signature(
195
+ relations: list[TopicRelation],
196
+ ) -> list[tuple[int, str, tuple[str, ...]]]:
197
+ """返回用于比较不同阶段主题关系的稳定表示。"""
198
+
199
+ return [
200
+ (relation.order, relation.topic, relation.prerequisites)
201
+ for relation in relations
202
+ ]