workflow-loop 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- workflow_loop/__init__.py +6 -0
- workflow_loop/acceptance_records.py +338 -0
- workflow_loop/artifact_paths.py +278 -0
- workflow_loop/artifact_validation.py +1738 -0
- workflow_loop/bug_record.py +203 -0
- workflow_loop/cli.py +3257 -0
- workflow_loop/data/Standardized_Repository/acceptance/acceptance.md +119 -0
- workflow_loop/data/Standardized_Repository/acceptance/acceptance_plan.md +105 -0
- workflow_loop/data/Standardized_Repository/code_design/code_design.md +204 -0
- workflow_loop/data/Standardized_Repository/code_design/project_design_init.md +152 -0
- workflow_loop/data/Standardized_Repository/code_design/revise_code_design.md +32 -0
- workflow_loop/data/Standardized_Repository/code_design/update_code_design.md +94 -0
- workflow_loop/data/Standardized_Repository/global/document_writing.md +77 -0
- workflow_loop/data/Standardized_Repository/global/workflow_lifecycle.md +91 -0
- workflow_loop/data/Standardized_Repository/impl/code_implementation.md +85 -0
- workflow_loop/data/Standardized_Repository/impl/impl.md +164 -0
- workflow_loop/data/Standardized_Repository/qa/test.md +167 -0
- workflow_loop/data/Standardized_Repository/qa/test_code.md +121 -0
- workflow_loop/data/Standardized_Repository/qa/test_code_implementation.md +67 -0
- workflow_loop/data/Standardized_Repository/qa/test_plan.md +160 -0
- workflow_loop/data/Standardized_Repository/reproduce/reproduce.md +60 -0
- workflow_loop/data/Standardized_Repository/spec/spec.md +138 -0
- workflow_loop/data/Standardized_Repository/spike/spike.md +236 -0
- workflow_loop/data/Template_Repository/acceptance/acceptance_plan.md +142 -0
- workflow_loop/data/Template_Repository/acceptance/acceptance_result.md +108 -0
- workflow_loop/data/Template_Repository/code_design/code_design.md +260 -0
- workflow_loop/data/Template_Repository/code_design/project_design_init_evidence.md +39 -0
- workflow_loop/data/Template_Repository/impl/impl.md +112 -0
- workflow_loop/data/Template_Repository/qa/test.md +102 -0
- workflow_loop/data/Template_Repository/qa/test_plan.md +100 -0
- workflow_loop/data/Template_Repository/reproduce/reproduce.md +82 -0
- workflow_loop/data/Template_Repository/spec/spec.md +222 -0
- workflow_loop/data/Template_Repository/spike/spike.md +135 -0
- workflow_loop/installer.py +632 -0
- workflow_loop/journal.py +78 -0
- workflow_loop/path_composer.py +152 -0
- workflow_loop/process_runner.py +176 -0
- workflow_loop/project.py +397 -0
- workflow_loop/role_doc.py +133 -0
- workflow_loop/rollback.py +1738 -0
- workflow_loop/spike_validation.py +379 -0
- workflow_loop/stage_materials.py +169 -0
- workflow_loop/stages/__init__.py +45 -0
- workflow_loop/stages/base.py +164 -0
- workflow_loop/stages/stages.py +1191 -0
- workflow_loop/state.py +582 -0
- workflow_loop/test_entry.py +123 -0
- workflow_loop/test_execution.py +619 -0
- workflow_loop/test_mapping.py +568 -0
- workflow_loop/test_runner.py +134 -0
- workflow_loop/topic.py +114 -0
- workflow_loop/topic_relations.py +202 -0
- workflow_loop/traceability.py +533 -0
- workflow_loop/verification.py +971 -0
- workflow_loop-0.1.0.dist-info/METADATA +187 -0
- workflow_loop-0.1.0.dist-info/RECORD +60 -0
- workflow_loop-0.1.0.dist-info/WHEEL +5 -0
- workflow_loop-0.1.0.dist-info/entry_points.txt +2 -0
- workflow_loop-0.1.0.dist-info/licenses/LICENSE +21 -0
- workflow_loop-0.1.0.dist-info/top_level.txt +1 -0
workflow_loop/project.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from dataclasses import dataclass, asdict, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
# 统一产品身份:版本常量只在包 __init__ 定义一份,安装、CLI 和项目检查共用
|
|
9
|
+
from . import __version__ as PRODUCT_VERSION
|
|
10
|
+
|
|
11
|
+
# project.json 的相对路径(相对于项目根)
|
|
12
|
+
# 放在 .workflow_loop/ 下,和 state.json、journal.jsonl 同级
|
|
13
|
+
# 但 project.json 是项目级持久字段,跨 Run 不被新 Run 覆盖
|
|
14
|
+
PROJECT_FILE = os.path.join(".workflow_loop", "project.json")
|
|
15
|
+
|
|
16
|
+
# 安装器版本号,用于重复安装保护判断
|
|
17
|
+
# 严格等于产品版本 0.1.0,不存在兼容版本范围
|
|
18
|
+
INSTALLER_VERSION = PRODUCT_VERSION
|
|
19
|
+
# 项目全量测试入口是"操作系统 → 命令参数数组"的映射;新安装项目默认为空。
|
|
20
|
+
# 旧开发状态可能仍是字符串脚本路径;读取时原样保留,由受控迁移转换。
|
|
21
|
+
DEFAULT_TEST_ENTRY: dict = {}
|
|
22
|
+
DEFAULT_TEST_PARALLELISM = 2
|
|
23
|
+
|
|
24
|
+
# 完整项目骨架的组成部分:安装时一次写入,日常检查必须全部存在
|
|
25
|
+
WORKFLOW_LOOP_DIRNAME = ".workflow_loop"
|
|
26
|
+
TEMPLATE_DIRNAME = "Template_Repository"
|
|
27
|
+
STANDARDIZED_DIRNAME = "Standardized_Repository"
|
|
28
|
+
AGENTS_MD_FILENAME = "AGENTS.md"
|
|
29
|
+
MANAGED_PROJECT_FIELDS = (
|
|
30
|
+
"project_design_initialized",
|
|
31
|
+
"topic_history",
|
|
32
|
+
"test_entry",
|
|
33
|
+
"test_parallelism",
|
|
34
|
+
"artifact_file_keys",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# 项目级持久状态(跨 Run 不被覆盖)
|
|
39
|
+
# 和 WorkflowState(单次 Run 快照,新 Run 整份覆盖)分离
|
|
40
|
+
# project_design_initialized 不放 state.json,因为它跨 Run 持久
|
|
41
|
+
@dataclass
|
|
42
|
+
class ProjectState:
|
|
43
|
+
# 安装器版本号,安装时写入
|
|
44
|
+
# is_installed() 检查这个字段判断项目是否已安装
|
|
45
|
+
installer_version: str = INSTALLER_VERSION
|
|
46
|
+
# 安装时间 ISO 8601 UTC,用于追溯项目何时接入 workflow_loop
|
|
47
|
+
installed_at: str = ""
|
|
48
|
+
# 项目设计架构初始化标记
|
|
49
|
+
# 安装时 false;project_design_init stage --confirmed 后置 true
|
|
50
|
+
# from_scratch 在 spec + code_design 都 --confirmed 后置 true
|
|
51
|
+
# PathComposer 用这个字段决定 product_change/bugfix 是否前置 project_design_init
|
|
52
|
+
project_design_initialized: bool = False
|
|
53
|
+
# 已经确认过的验收主题名称。修 bug 在 reproduce 确认,其他意图在 acceptance_plan 确认。
|
|
54
|
+
topic_history: list[str] = field(default_factory=list)
|
|
55
|
+
# 项目统一全量测试入口:操作系统(windows/linux/darwin/default)到参数数组的映射。
|
|
56
|
+
# 不再把当前仓库脚本当成所有项目默认;旧字符串配置读取时原样保留,等待受控迁移。
|
|
57
|
+
test_entry: dict | str = field(default_factory=dict)
|
|
58
|
+
# 主题测试执行阶段最多同时运行多少个独立主题;同一主题内仍按测试项依赖顺序执行。
|
|
59
|
+
test_parallelism: int = DEFAULT_TEST_PARALLELISM
|
|
60
|
+
# 显示名称到稳定中文文件标识的项目级映射,按 feature/topic/spike/bug 分组。
|
|
61
|
+
# 显示名称仍写入正文;文件标识只进入文件名。跨轮次保存,不随新 Run 覆盖。
|
|
62
|
+
artifact_file_keys: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# 项目骨架检查结果:
|
|
66
|
+
# state 取值 "installed"(完整安装 0.1.0)/ "uninstalled"(干净未安装)/ "broken"(残缺或版本异常)
|
|
67
|
+
# problems 保存 broken 时的具体缺项,供安装脚本和日常命令直接打印
|
|
68
|
+
@dataclass
|
|
69
|
+
class SkeletonStatus:
|
|
70
|
+
state: str
|
|
71
|
+
problems: list[str] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# 生成 ISO 8601 UTC 时间戳(内部用,不对外暴露)
|
|
75
|
+
# 和 state.py 的 now_iso() 逻辑一致,但独立定义避免循环导入
|
|
76
|
+
def _now_iso() -> str:
|
|
77
|
+
# datetime.now(timezone.utc) 拿到 UTC 时间
|
|
78
|
+
# .replace(microsecond=0) 去掉微秒
|
|
79
|
+
# .isoformat() 转 ISO 8601 字符串
|
|
80
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# 从被管理项目的 .workflow_loop/project.json 读取项目级状态
|
|
84
|
+
# 如果文件不存在(还没安装),返回 None
|
|
85
|
+
def load_project(project_root: str) -> ProjectState | None:
|
|
86
|
+
# 拼出 project.json 的完整路径
|
|
87
|
+
path = os.path.join(project_root, PROJECT_FILE)
|
|
88
|
+
# 文件不存在说明项目还没安装
|
|
89
|
+
if not os.path.exists(path):
|
|
90
|
+
return None
|
|
91
|
+
# 读文件、解析 JSON
|
|
92
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
93
|
+
data = json.load(f)
|
|
94
|
+
# 重建 ProjectState dataclass
|
|
95
|
+
raw_keys = data.get("artifact_file_keys", {})
|
|
96
|
+
artifact_file_keys = {
|
|
97
|
+
category: dict(mapping)
|
|
98
|
+
for category, mapping in raw_keys.items()
|
|
99
|
+
if isinstance(mapping, dict)
|
|
100
|
+
}
|
|
101
|
+
return ProjectState(
|
|
102
|
+
installer_version=data.get("installer_version", ""),
|
|
103
|
+
installed_at=data.get("installed_at", ""),
|
|
104
|
+
project_design_initialized=data.get("project_design_initialized", False),
|
|
105
|
+
topic_history=data.get("topic_history", []),
|
|
106
|
+
test_entry=data.get("test_entry", DEFAULT_TEST_ENTRY),
|
|
107
|
+
test_parallelism=max(1, int(data.get("test_parallelism", DEFAULT_TEST_PARALLELISM))),
|
|
108
|
+
artifact_file_keys=artifact_file_keys,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _atomic_write_json(path: str, data: dict) -> None:
|
|
113
|
+
"""在目标文件同目录写完整临时文件,再原子替换目标。"""
|
|
114
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
115
|
+
handle = tempfile.NamedTemporaryFile(
|
|
116
|
+
mode="w",
|
|
117
|
+
encoding="utf-8",
|
|
118
|
+
prefix=".project-",
|
|
119
|
+
suffix=".tmp",
|
|
120
|
+
dir=os.path.dirname(path),
|
|
121
|
+
delete=False,
|
|
122
|
+
)
|
|
123
|
+
temp_path = handle.name
|
|
124
|
+
try:
|
|
125
|
+
with handle:
|
|
126
|
+
json.dump(data, handle, ensure_ascii=False, indent=2)
|
|
127
|
+
handle.flush()
|
|
128
|
+
os.fsync(handle.fileno())
|
|
129
|
+
os.replace(temp_path, path)
|
|
130
|
+
finally:
|
|
131
|
+
if os.path.exists(temp_path):
|
|
132
|
+
os.remove(temp_path)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# 把 ProjectState 写到被管理项目的 .workflow_loop/project.json
|
|
136
|
+
# ensure_ascii=False 让中文不被转义;indent=2 让文件可读
|
|
137
|
+
def save_project(project_root: str, project: ProjectState) -> None:
|
|
138
|
+
path = os.path.join(project_root, PROJECT_FILE)
|
|
139
|
+
# 保留未来版本或项目扩展写入的未知字段;当前版本只更新自己拥有的键。
|
|
140
|
+
data = {}
|
|
141
|
+
if os.path.isfile(path):
|
|
142
|
+
try:
|
|
143
|
+
with open(path, "r", encoding="utf-8") as stream:
|
|
144
|
+
existing = json.load(stream)
|
|
145
|
+
if isinstance(existing, dict):
|
|
146
|
+
data.update(existing)
|
|
147
|
+
except (OSError, json.JSONDecodeError):
|
|
148
|
+
data = {}
|
|
149
|
+
data.update(asdict(project))
|
|
150
|
+
_atomic_write_json(path, data)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _validate_managed_fields(fields: dict) -> None:
|
|
154
|
+
if not isinstance(fields, dict):
|
|
155
|
+
raise ValueError("项目受管字段快照必须是 JSON 对象")
|
|
156
|
+
missing = [name for name in MANAGED_PROJECT_FIELDS if name not in fields]
|
|
157
|
+
if missing:
|
|
158
|
+
raise ValueError(f"项目受管字段快照缺少字段: {missing}")
|
|
159
|
+
if not isinstance(fields["project_design_initialized"], bool):
|
|
160
|
+
raise ValueError("project_design_initialized(项目设计已初始化)必须是布尔值")
|
|
161
|
+
if (
|
|
162
|
+
not isinstance(fields["topic_history"], list)
|
|
163
|
+
or not all(isinstance(item, str) for item in fields["topic_history"])
|
|
164
|
+
):
|
|
165
|
+
raise ValueError("topic_history(历史主题)必须是字符串数组")
|
|
166
|
+
if not isinstance(fields["test_entry"], (dict, str)):
|
|
167
|
+
raise ValueError("test_entry(项目全量测试入口)必须是平台映射或旧字符串")
|
|
168
|
+
if (
|
|
169
|
+
not isinstance(fields["test_parallelism"], int)
|
|
170
|
+
or isinstance(fields["test_parallelism"], bool)
|
|
171
|
+
or fields["test_parallelism"] < 1
|
|
172
|
+
):
|
|
173
|
+
raise ValueError("test_parallelism(主题测试最大并行数)必须是正整数")
|
|
174
|
+
mappings = fields["artifact_file_keys"]
|
|
175
|
+
if not isinstance(mappings, dict):
|
|
176
|
+
raise ValueError("artifact_file_keys(正式文件标识映射)必须是对象")
|
|
177
|
+
for category, mapping in mappings.items():
|
|
178
|
+
if not isinstance(category, str) or not isinstance(mapping, dict):
|
|
179
|
+
raise ValueError("正式文件标识映射的分类和值必须是对象")
|
|
180
|
+
if not all(
|
|
181
|
+
isinstance(display_name, str) and isinstance(file_key, str)
|
|
182
|
+
for display_name, file_key in mapping.items()
|
|
183
|
+
):
|
|
184
|
+
raise ValueError("正式文件标识映射的显示名称和文件标识必须是字符串")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def snapshot_managed_fields(project_root: str) -> dict:
|
|
188
|
+
"""保存本轮可能修改的项目级字段,不包含安装身份等无关字段。"""
|
|
189
|
+
project = load_project(project_root)
|
|
190
|
+
if project is None:
|
|
191
|
+
raise ValueError(f"缺少 {PROJECT_FILE},不能保存项目受管字段")
|
|
192
|
+
fields = {
|
|
193
|
+
"project_design_initialized": project.project_design_initialized,
|
|
194
|
+
"topic_history": list(project.topic_history),
|
|
195
|
+
"test_entry": copy.deepcopy(project.test_entry),
|
|
196
|
+
"test_parallelism": project.test_parallelism,
|
|
197
|
+
"artifact_file_keys": copy.deepcopy(project.artifact_file_keys),
|
|
198
|
+
}
|
|
199
|
+
_validate_managed_fields(fields)
|
|
200
|
+
return fields
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def restore_managed_fields(project_root: str, fields: dict) -> None:
|
|
204
|
+
"""只恢复本轮受管字段,保留安装身份和未知扩展字段。"""
|
|
205
|
+
_validate_managed_fields(fields)
|
|
206
|
+
path = os.path.join(project_root, PROJECT_FILE)
|
|
207
|
+
if not os.path.isfile(path):
|
|
208
|
+
raise ValueError(f"缺少 {PROJECT_FILE},不能恢复项目受管字段")
|
|
209
|
+
try:
|
|
210
|
+
with open(path, "r", encoding="utf-8") as stream:
|
|
211
|
+
data = json.load(stream)
|
|
212
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
213
|
+
raise ValueError(f"{PROJECT_FILE} 无法读取,不能恢复项目受管字段: {exc}") from exc
|
|
214
|
+
if not isinstance(data, dict):
|
|
215
|
+
raise ValueError(f"{PROJECT_FILE} 必须是 JSON 对象")
|
|
216
|
+
|
|
217
|
+
for name in MANAGED_PROJECT_FIELDS:
|
|
218
|
+
data[name] = copy.deepcopy(fields[name])
|
|
219
|
+
_atomic_write_json(path, data)
|
|
220
|
+
if snapshot_managed_fields(project_root) != fields:
|
|
221
|
+
raise ValueError("项目受管字段写回后复核不一致")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def register_artifact_names(
|
|
225
|
+
project_root: str,
|
|
226
|
+
category: str,
|
|
227
|
+
display_names: list[str],
|
|
228
|
+
) -> dict[str, str]:
|
|
229
|
+
"""登记正式产物的显示名称到稳定文件标识,并在有新增时落盘。"""
|
|
230
|
+
from . import artifact_paths as artifact_paths_mod
|
|
231
|
+
|
|
232
|
+
project = load_project(project_root)
|
|
233
|
+
if project is None:
|
|
234
|
+
raise ValueError("项目尚未安装,不能登记正式文件标识")
|
|
235
|
+
added = artifact_paths_mod.register_file_keys(project, category, display_names)
|
|
236
|
+
if added:
|
|
237
|
+
save_project(project_root, project)
|
|
238
|
+
return added
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _required_repository_files(dirname: str) -> list[str]:
|
|
242
|
+
"""返回当前产品发布要求目标项目具备的仓库文件。"""
|
|
243
|
+
source_root = os.path.join(os.path.dirname(__file__), "data", dirname)
|
|
244
|
+
required: list[str] = []
|
|
245
|
+
if not os.path.isdir(source_root):
|
|
246
|
+
return required
|
|
247
|
+
for root, _dirs, files in os.walk(source_root):
|
|
248
|
+
for filename in files:
|
|
249
|
+
relative = os.path.relpath(os.path.join(root, filename), source_root)
|
|
250
|
+
required.append(relative.replace(os.sep, "/"))
|
|
251
|
+
return sorted(required)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# 检查项目骨架完整性,区分三种状态:
|
|
255
|
+
# - installed:项目标记、模板仓库、规范仓库和 AGENTS.md 共同构成完整骨架,且版本严格等于 0.1.0
|
|
256
|
+
# - uninstalled:.workflow_loop/ 完全不存在(干净目录,可以首次安装)
|
|
257
|
+
# - broken:骨架部分存在、版本不符或 project.json 无法读取;任何写入前必须停止
|
|
258
|
+
def check_skeleton(project_root: str) -> SkeletonStatus:
|
|
259
|
+
wf_dir = os.path.join(project_root, WORKFLOW_LOOP_DIRNAME)
|
|
260
|
+
project_json = os.path.join(project_root, PROJECT_FILE)
|
|
261
|
+
|
|
262
|
+
# .workflow_loop/ 完全不存在 → 干净未安装
|
|
263
|
+
if not os.path.exists(wf_dir):
|
|
264
|
+
return SkeletonStatus(state="uninstalled")
|
|
265
|
+
|
|
266
|
+
problems: list[str] = []
|
|
267
|
+
|
|
268
|
+
# project.json:必须存在、可解析、版本严格等于 0.1.0
|
|
269
|
+
if not os.path.isfile(project_json):
|
|
270
|
+
problems.append(f"缺少安装版本标记 {PROJECT_FILE}")
|
|
271
|
+
else:
|
|
272
|
+
try:
|
|
273
|
+
with open(project_json, "r", encoding="utf-8") as f:
|
|
274
|
+
data = json.load(f)
|
|
275
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
276
|
+
problems.append(f"{PROJECT_FILE} 无法读取: {exc}")
|
|
277
|
+
else:
|
|
278
|
+
if not isinstance(data, dict):
|
|
279
|
+
problems.append(f"{PROJECT_FILE} 顶层必须是 JSON 对象")
|
|
280
|
+
else:
|
|
281
|
+
version = data.get("installer_version")
|
|
282
|
+
if version != INSTALLER_VERSION:
|
|
283
|
+
problems.append(
|
|
284
|
+
f"安装版本标记是 {version!r},当前产品只接受 {INSTALLER_VERSION!r}"
|
|
285
|
+
)
|
|
286
|
+
if not isinstance(data.get("project_design_initialized", False), bool):
|
|
287
|
+
problems.append("project_design_initialized(项目设计已初始化)字段无效")
|
|
288
|
+
if not isinstance(data.get("topic_history", []), list):
|
|
289
|
+
problems.append("topic_history(历史主题)字段无效")
|
|
290
|
+
if not isinstance(data.get("test_entry", {}), (dict, str)):
|
|
291
|
+
problems.append("test_entry(项目全量测试入口)字段无效")
|
|
292
|
+
if not isinstance(data.get("artifact_file_keys", {}), dict):
|
|
293
|
+
problems.append("artifact_file_keys(正式文件标识映射)字段无效")
|
|
294
|
+
|
|
295
|
+
# 模板仓库和规范仓库:当前产品发布中的每一份必需材料都必须存在。
|
|
296
|
+
# 允许项目增加其它文件,也允许在工作流内按规则修改正文,但不能缺文件。
|
|
297
|
+
for dirname in (TEMPLATE_DIRNAME, STANDARDIZED_DIRNAME):
|
|
298
|
+
dir_path = os.path.join(wf_dir, dirname)
|
|
299
|
+
if not os.path.isdir(dir_path):
|
|
300
|
+
problems.append(f"缺少 .workflow_loop/{dirname}/")
|
|
301
|
+
continue
|
|
302
|
+
required_files = _required_repository_files(dirname)
|
|
303
|
+
if not required_files:
|
|
304
|
+
problems.append(f"当前安装包没有携带 {dirname} 文件清单")
|
|
305
|
+
continue
|
|
306
|
+
for relative_path in required_files:
|
|
307
|
+
full_path = os.path.join(dir_path, *relative_path.split("/"))
|
|
308
|
+
if not os.path.isfile(full_path):
|
|
309
|
+
problems.append(f"缺少 .workflow_loop/{dirname}/{relative_path}")
|
|
310
|
+
|
|
311
|
+
# 最小代理契约:必须存在于项目根
|
|
312
|
+
if not os.path.isfile(os.path.join(project_root, AGENTS_MD_FILENAME)):
|
|
313
|
+
problems.append(f"缺少项目根 {AGENTS_MD_FILENAME}")
|
|
314
|
+
|
|
315
|
+
if problems:
|
|
316
|
+
return SkeletonStatus(state="broken", problems=problems)
|
|
317
|
+
return SkeletonStatus(state="installed")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# 判断项目设计架构是否已初始化(PathComposer 用)
|
|
321
|
+
# true → product_change/bugfix 跳过 project_design_init stage
|
|
322
|
+
# false → 必须执行 project_design_init
|
|
323
|
+
# 不用架构文档是否存在决定跳过(CONTEXT.md "Project Design Init Skip")
|
|
324
|
+
def is_project_design_initialized(project_root: str) -> bool:
|
|
325
|
+
# 读 project.json
|
|
326
|
+
project = load_project(project_root)
|
|
327
|
+
# project 存在且 project_design_initialized 为 true 才返回 True
|
|
328
|
+
return project is not None and project.project_design_initialized
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# 设置项目设计架构初始化标记
|
|
332
|
+
# 在 project_design_init stage --confirmed 后置 true
|
|
333
|
+
# 在 from_scratch 的 spec + code_design 都 --confirmed 后置 true
|
|
334
|
+
# 在 from_scratch start 时重置为 false(清场后重新做)
|
|
335
|
+
def set_project_design_initialized(project_root: str, value: bool) -> None:
|
|
336
|
+
# 读当前 project 状态
|
|
337
|
+
project = load_project(project_root)
|
|
338
|
+
# project 不存在(异常情况)→ 新建一个,带当前时间戳
|
|
339
|
+
if project is None:
|
|
340
|
+
project = ProjectState(installed_at=_now_iso())
|
|
341
|
+
# 更新标记
|
|
342
|
+
project.project_design_initialized = value
|
|
343
|
+
# 写回 project.json
|
|
344
|
+
save_project(project_root, project)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def register_topics(project_root: str, topics: list[str]) -> None:
|
|
348
|
+
"""原子登记本次主题历史及对应的稳定正式文件标识。"""
|
|
349
|
+
from . import artifact_paths as artifact_paths_mod
|
|
350
|
+
|
|
351
|
+
project = load_project(project_root)
|
|
352
|
+
if project is None:
|
|
353
|
+
project = ProjectState(installed_at=_now_iso())
|
|
354
|
+
|
|
355
|
+
duplicates = sorted(set(topics) & set(project.topic_history))
|
|
356
|
+
if duplicates:
|
|
357
|
+
raise ValueError(f"主题名称已经使用过: {duplicates}")
|
|
358
|
+
|
|
359
|
+
artifact_paths_mod.register_file_keys(project, "topic", topics)
|
|
360
|
+
project.topic_history.extend(topic for topic in topics if topic not in project.topic_history)
|
|
361
|
+
save_project(project_root, project)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def register_test_entry(project_root: str, entry_config: dict) -> None:
|
|
365
|
+
"""更新项目全量测试入口配置;只应由入口登记命令在测试计划阶段调用。"""
|
|
366
|
+
from . import test_entry as test_entry_mod
|
|
367
|
+
|
|
368
|
+
normalized = test_entry_mod.normalized_entry_config(entry_config)
|
|
369
|
+
project = load_project(project_root)
|
|
370
|
+
if project is None:
|
|
371
|
+
raise ValueError("项目尚未安装,不能登记测试入口")
|
|
372
|
+
project.test_entry = normalized
|
|
373
|
+
save_project(project_root, project)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# 判断项目是否已安装(安装事务和 start 命令用)
|
|
377
|
+
# 完整骨架 + 版本严格等于 0.1.0 才算已安装;残缺骨架不算
|
|
378
|
+
# 未安装时 start 命令报错,提示用户先跑官方安装脚本
|
|
379
|
+
def is_installed(project_root: str) -> bool:
|
|
380
|
+
return check_skeleton(project_root).state == "installed"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# 创建项目级状态(installer.py 在安装事务中调用)
|
|
384
|
+
# 只写精简项目级字段;不创建正式产物目录,也不下发全量测试脚本
|
|
385
|
+
# 初始 project_design_initialized=false,后续由 project_design_init stage 更新
|
|
386
|
+
def create_project(project_root: str) -> ProjectState:
|
|
387
|
+
# 新建 ProjectState,带当前版本号和时间戳
|
|
388
|
+
project = ProjectState(
|
|
389
|
+
installer_version=INSTALLER_VERSION,
|
|
390
|
+
installed_at=_now_iso(),
|
|
391
|
+
# 初始为 false,需要走 project_design_init stage 才置 true
|
|
392
|
+
project_design_initialized=False,
|
|
393
|
+
)
|
|
394
|
+
# 写到 project.json
|
|
395
|
+
save_project(project_root, project)
|
|
396
|
+
# 返回新建的 project,供调用方使用
|
|
397
|
+
return project
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# 文档概览文本(start 命令和 status 命令可能打印)
|
|
2
|
+
# 给 AI 和用户介绍项目文档结构:spec/bug/qa/acceptance/impl 各是什么、命名规则
|
|
3
|
+
# AI 读完理解项目文档全貌,后面每个 stage 基于这个理解工作
|
|
4
|
+
DOC_OVERVIEW = """═══ 文档概览 ═══
|
|
5
|
+
|
|
6
|
+
本项目使用 workflow_loop 工作流管理。正式产物使用中文文件名;
|
|
7
|
+
spec、acceptance、qa、impl、bug 等程序固定目录名保持英文。文档结构:
|
|
8
|
+
|
|
9
|
+
【spec/】产品与设计
|
|
10
|
+
- 产品总说明.md:产品设计说明书 + 功能清单
|
|
11
|
+
- 功能_<功能文件标识>.md:按功能拆分的功能设计文档;标题保留完整显示名称
|
|
12
|
+
- 代码架构设计.md:代码架构设计文档
|
|
13
|
+
- 穿刺清单.md / 穿刺_<穿刺项文件标识>.md:穿刺清单与结论
|
|
14
|
+
- 项目设计初始化证据.md:存量项目初始化的调查证据
|
|
15
|
+
|
|
16
|
+
【bug/】缺陷记录
|
|
17
|
+
- 缺陷_<缺陷文件标识>.md:单个缺陷记录
|
|
18
|
+
- 索引.md:缺陷索引表
|
|
19
|
+
|
|
20
|
+
【qa/】测试
|
|
21
|
+
- 索引.md:测试索引表
|
|
22
|
+
- <主题文件标识>_测试计划.md:主题测试计划
|
|
23
|
+
- <主题文件标识>_测试结果.md:主题测试结果
|
|
24
|
+
- state.json / journal.jsonl:记录最终全量回归的命令、退出码、状态和输出摘要
|
|
25
|
+
|
|
26
|
+
【acceptance/】验收
|
|
27
|
+
- 索引.md:验收主题关系和验收文档索引
|
|
28
|
+
- <主题文件标识>_验收计划.md:主题验收计划
|
|
29
|
+
- <主题文件标识>_验收结果.md:主题验收结果
|
|
30
|
+
|
|
31
|
+
【需求交付追踪表.md】需求交付追踪表
|
|
32
|
+
- 按工作流编号记录需求、验收主题、验收条件和后续阶段的对应位置
|
|
33
|
+
|
|
34
|
+
【impl/】实施记录
|
|
35
|
+
- 索引.md:继承验收主题关系的实施索引
|
|
36
|
+
- <主题文件标识>_实施记录.md:每个验收主题一份实施计划和实施记录
|
|
37
|
+
|
|
38
|
+
验收计划、测试计划、实施记录和主题测试/验收结果使用同一个主题显示名称和文件标识。
|
|
39
|
+
从零开发和修改产品在 acceptance_plan(验收计划)环节确定主题;修 bug 在 reproduce(缺陷复现)环节确定主题。"""
|
|
40
|
+
|
|
41
|
+
# stage 名 → 角色定义的映射表
|
|
42
|
+
# discuss 命令用 get_role_doc(stage_name) 拿角色定义,打印给 AI 看
|
|
43
|
+
# 角色定义告诉 AI:"这个 stage 你是什么角色、要产出什么文件、文件放哪"
|
|
44
|
+
# 加新 stage 时在这里加一行映射即可
|
|
45
|
+
ROLE_DOC_MAP = {
|
|
46
|
+
# 产品设计阶段:从零建立或改产品重新设计
|
|
47
|
+
"spec": {
|
|
48
|
+
"role": "产品设计师",
|
|
49
|
+
"description": "和用户讨论产品要做什么、功能清单、功能拆分。产出 spec/产品总说明.md 和每个功能的 spec/功能_<功能文件标识>.md;文档标题写完整功能名称,文件名使用稳定中文文件标识。",
|
|
50
|
+
},
|
|
51
|
+
# 从零做的初步代码架构阶段:从已确认产品设计推导代码设计
|
|
52
|
+
"code_design": {
|
|
53
|
+
"role": "代码架构设计师(初步)",
|
|
54
|
+
"description": "从已确认的产品设计推导代码分层、架构关键节点和各功能的完整代码过程,产出 spec/代码架构设计.md。",
|
|
55
|
+
},
|
|
56
|
+
# 穿刺阶段:验证真实场景中的技术不确定性
|
|
57
|
+
"spike": {
|
|
58
|
+
"role": "技术不确定性验证工程师",
|
|
59
|
+
"description": "先查看产品设计、代码设计、相关代码和运行事实,找出必须用真实场景确认的技术不确定性。用户决定执行哪些穿刺项;验证完成后在 spec/穿刺清单.md 和 spec/穿刺_<穿刺项文件标识>.md 记录真实证据,并把结论同步到受影响的设计文档。",
|
|
60
|
+
},
|
|
61
|
+
# 存量项目首次初始化:一次建立产品+功能+架构三类产物
|
|
62
|
+
"project_design_init": {
|
|
63
|
+
"role": "存量产品与架构分析师",
|
|
64
|
+
"description": "首次处理已有代码项目时,必须查看代码和测试,具备安全条件时实际运行,一次建立相互一致的 spec/产品总说明.md、spec/功能_*.md、spec/代码架构设计.md 和 spec/项目设计初始化证据.md。",
|
|
65
|
+
},
|
|
66
|
+
# 改产品设计期架构修订:按新设计改架构图
|
|
67
|
+
"revise_code_design": {
|
|
68
|
+
"role": "架构文档修订者",
|
|
69
|
+
"description": "改产品路径上,按变更后的产品设计修改 spec/代码架构设计.md。",
|
|
70
|
+
},
|
|
71
|
+
# 末段最终设计同步:核对产品、功能、架构和真实代码映射
|
|
72
|
+
"update_code_design": {
|
|
73
|
+
"role": "最终设计同步者",
|
|
74
|
+
"description": "最终全量回归和整体验收通过后,核对产品总说明、功能文档、代码架构设计和真实代码;架构有变化时更新架构与功能到代码的映射,发现功能变化返回 spec(产品设计),发现代码未实现返回 impl(代码实施)。",
|
|
75
|
+
},
|
|
76
|
+
# 验收计划制定阶段
|
|
77
|
+
"acceptance_plan": {
|
|
78
|
+
"role": "验收计划制定者",
|
|
79
|
+
"description": "根据已确认需求确定或复用全部验收主题,确认主题前置关系,并为每个主题制定什么算完成。产出 需求交付追踪表.md、acceptance/索引.md 和 acceptance/<主题文件标识>_验收计划.md。",
|
|
80
|
+
},
|
|
81
|
+
# 测试计划制定阶段
|
|
82
|
+
"test_plan": {
|
|
83
|
+
"role": "测试计划制定者",
|
|
84
|
+
"description": "对已确认验收条件做测试覆盖审查,登记项目全量测试入口。产出 qa/<主题文件标识>_测试计划.md 和 qa/索引.md;本阶段不执行测试。",
|
|
85
|
+
},
|
|
86
|
+
# 实施执行阶段:改真实代码
|
|
87
|
+
"impl": {
|
|
88
|
+
"role": "实施执行者",
|
|
89
|
+
"description": "先根据验收计划和测试计划为全部主题确认实施前计划,再修改真实代码,并在 impl/<主题文件标识>_实施记录.md 中记录实际实施结果。",
|
|
90
|
+
},
|
|
91
|
+
# 测试代码阶段:调查真实代码并写测试代码
|
|
92
|
+
"test_code": {
|
|
93
|
+
"role": "测试代码编写者",
|
|
94
|
+
"description": "先读取验收计划、测试计划、实施记录和真实代码,确认每个测试项的代码落点,再编写带 Workflow-Test 标识的测试代码。可以运行与当前修改直接相关的局部测试作为开发反馈,但不产出正式测试结果。",
|
|
95
|
+
},
|
|
96
|
+
# 测试执行阶段:运行测试代码并记录结果
|
|
97
|
+
"test_execution": {
|
|
98
|
+
"role": "测试执行者",
|
|
99
|
+
"description": "逐项登记并运行 test_code 阶段写好的测试,按照 qa/<主题文件标识>_测试计划.md 记录真实机器执行结果,产出 qa/<主题文件标识>_测试结果.md。",
|
|
100
|
+
},
|
|
101
|
+
# 主题验收阶段:测试通过后核对用户结果
|
|
102
|
+
"topic_acceptance": {
|
|
103
|
+
"role": "主题验收执行者",
|
|
104
|
+
"description": "确认对应主题的测试结果通过后,按照 acceptance/<主题文件标识>_验收计划.md 逐条核对用户结果,产出 acceptance/<主题文件标识>_验收结果.md。",
|
|
105
|
+
},
|
|
106
|
+
"regression_test": {
|
|
107
|
+
"role": "最终回归测试执行者",
|
|
108
|
+
"description": "全部主题完成后,执行项目配置的统一测试入口。退出码为 0 才算回归通过;状态写入 state.json、journal 和 需求交付追踪表.md。",
|
|
109
|
+
},
|
|
110
|
+
"overall_acceptance": {
|
|
111
|
+
"role": "整体验收执行者",
|
|
112
|
+
"description": "代码先复核全部主题验收和最终全量回归已经通过,再由用户确认整个需求是否完成。本阶段不生成新的结果文档。",
|
|
113
|
+
},
|
|
114
|
+
# bug 复现阶段:复现+根因分析
|
|
115
|
+
"reproduce": {
|
|
116
|
+
"role": "bug 复现者",
|
|
117
|
+
"description": "使用真实场景复现缺陷并确认根因,根据修复后用户应该恢复得到的结果确定一个验收主题,产出 bug/缺陷_<缺陷文件标识>.md 并更新 bug/索引.md。",
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# 根据 stage 名拿角色定义
|
|
123
|
+
# discuss 命令调用:拿到 {role, description} dict,打印给 AI 看
|
|
124
|
+
# 返回 None 表示该 stage 没有角色定义(discuss 会打印"无角色定义")
|
|
125
|
+
def get_role_doc(stage_name: str) -> dict | None:
|
|
126
|
+
# 从映射表查找,找不到返回 None
|
|
127
|
+
return ROLE_DOC_MAP.get(stage_name)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# 返回文档概览文本
|
|
131
|
+
# start 命令和其它命令可能调用,打印给 AI 和用户看
|
|
132
|
+
def get_overview() -> str:
|
|
133
|
+
return DOC_OVERVIEW
|