opencode-collaboration 2.0.0__py3-none-any.whl → 2.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.
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/METADATA +1 -1
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/RECORD +14 -6
- src/core/config_reloader.py +257 -0
- src/core/design_review_notifier.py +303 -0
- src/core/error_templates.py +208 -0
- src/core/exception_handler.py +217 -2
- src/core/git_workflow_enforcer.py +352 -0
- src/core/iteration_status_manager.py +290 -0
- src/core/monitor.py +268 -0
- src/core/state_migrator.py +404 -0
- src/core/state_validator.py +564 -0
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/WHEEL +0 -0
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/entry_points.txt +0 -0
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Git 工作流强制约束模块。
|
|
2
|
+
|
|
3
|
+
功能:
|
|
4
|
+
1. 验证 Agent 通过 Git pull 获取最新文件
|
|
5
|
+
2. 强制执行 Git 操作
|
|
6
|
+
3. 检测工作流违规
|
|
7
|
+
"""
|
|
8
|
+
import subprocess
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Tuple, Optional, Dict, Any
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class WorkflowViolation:
|
|
21
|
+
"""工作流违规。"""
|
|
22
|
+
agent_id: str
|
|
23
|
+
action: str
|
|
24
|
+
reason: str
|
|
25
|
+
suggestion: str
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> Dict[str, str]:
|
|
28
|
+
return {
|
|
29
|
+
"agent_id": self.agent_id,
|
|
30
|
+
"action": self.action,
|
|
31
|
+
"reason": self.reason,
|
|
32
|
+
"suggestion": self.suggestion
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GitWorkflowEnforcer:
|
|
37
|
+
"""Git 工作流强制执行器。"""
|
|
38
|
+
|
|
39
|
+
REQUIRED_GIT_OPERATIONS = [
|
|
40
|
+
"READ_REQUIREMENTS",
|
|
41
|
+
"READ_DESIGN",
|
|
42
|
+
"READ_TEST_REPORT",
|
|
43
|
+
"READ_SIGNOFF",
|
|
44
|
+
"READ_CODE",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def __init__(self, project_path: str = "."):
|
|
48
|
+
"""初始化。
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
project_path: 项目路径
|
|
52
|
+
"""
|
|
53
|
+
self.project_path = Path(project_path)
|
|
54
|
+
|
|
55
|
+
def is_git_repository(self) -> bool:
|
|
56
|
+
"""检查是否为 Git 仓库。"""
|
|
57
|
+
return (self.project_path / ".git").exists()
|
|
58
|
+
|
|
59
|
+
def get_git_root(self) -> Optional[Path]:
|
|
60
|
+
"""获取 Git 根目录。"""
|
|
61
|
+
try:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
64
|
+
cwd=self.project_path,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=5
|
|
68
|
+
)
|
|
69
|
+
if result.returncode == 0:
|
|
70
|
+
return Path(result.stdout.strip())
|
|
71
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
72
|
+
pass
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def verify_git_pull(self, agent_id: str, file_path: str) -> Tuple[bool, Optional[WorkflowViolation]]:
|
|
76
|
+
"""验证 Agent 是否通过 Git pull 获取最新文件。
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
agent_id: Agent ID
|
|
80
|
+
file_path: 要读取的文件路径
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
(是否通过, 违规信息)
|
|
84
|
+
"""
|
|
85
|
+
abs_path = self.project_path / file_path
|
|
86
|
+
|
|
87
|
+
if not abs_path.exists():
|
|
88
|
+
return True, None
|
|
89
|
+
|
|
90
|
+
local_content = self._read_local_file(str(abs_path))
|
|
91
|
+
|
|
92
|
+
git_content = self._git_show(f"HEAD:{file_path}")
|
|
93
|
+
|
|
94
|
+
if git_content is None:
|
|
95
|
+
logger.warning(f"文件不在 Git 中: {file_path}")
|
|
96
|
+
return True, None
|
|
97
|
+
|
|
98
|
+
if local_content != git_content:
|
|
99
|
+
return False, WorkflowViolation(
|
|
100
|
+
agent_id=agent_id,
|
|
101
|
+
action="READ_FILE",
|
|
102
|
+
reason=f"本地文件与 Git HEAD 不一致",
|
|
103
|
+
suggestion="请先执行 'git pull' 获取最新版本"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return True, None
|
|
107
|
+
|
|
108
|
+
def verify_git_status(self, agent_id: str) -> Tuple[bool, Optional[WorkflowViolation]]:
|
|
109
|
+
"""验证工作区是否干净。
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
agent_id: Agent ID
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
(是否干净, 违规信息)
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
["git", "status", "--porcelain"],
|
|
120
|
+
cwd=self.project_path,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=5
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if result.stdout.strip():
|
|
127
|
+
return False, WorkflowViolation(
|
|
128
|
+
agent_id=agent_id,
|
|
129
|
+
action="GIT_STATUS",
|
|
130
|
+
reason="工作区有未提交的更改",
|
|
131
|
+
suggestion="请先提交或暂存更改"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return True, None
|
|
135
|
+
|
|
136
|
+
except subprocess.TimeoutExpired:
|
|
137
|
+
return False, WorkflowViolation(
|
|
138
|
+
agent_id=agent_id,
|
|
139
|
+
action="GIT_STATUS",
|
|
140
|
+
reason="Git 命令超时",
|
|
141
|
+
suggestion="检查网络连接和 Git 配置"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def enforce_git_operation(self, agent_id: str, operation: str) -> Tuple[bool, Optional[WorkflowViolation]]:
|
|
145
|
+
"""强制执行 Git 操作。
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
agent_id: Agent ID
|
|
149
|
+
operation: 操作类型
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
(是否通过, 违规信息)
|
|
153
|
+
"""
|
|
154
|
+
if operation in self.REQUIRED_GIT_OPERATIONS:
|
|
155
|
+
success, error = self._run_git_pull()
|
|
156
|
+
if not success:
|
|
157
|
+
return False, WorkflowViolation(
|
|
158
|
+
agent_id=agent_id,
|
|
159
|
+
action=operation,
|
|
160
|
+
reason="Git pull 失败",
|
|
161
|
+
suggestion=str(error) if error else "请检查 Git 配置"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return True, None
|
|
165
|
+
|
|
166
|
+
def check_file_in_git(self, file_path: str) -> bool:
|
|
167
|
+
"""检查文件是否在 Git 版本控制中。"""
|
|
168
|
+
try:
|
|
169
|
+
result = subprocess.run(
|
|
170
|
+
["git", "ls-files", "--error-unmatch", file_path],
|
|
171
|
+
cwd=self.project_path,
|
|
172
|
+
capture_output=True,
|
|
173
|
+
text=True,
|
|
174
|
+
timeout=5
|
|
175
|
+
)
|
|
176
|
+
return result.returncode == 0
|
|
177
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def get_file_version(self, file_path: str) -> Optional[str]:
|
|
181
|
+
"""获取文件的 Git HEAD 版本哈希。"""
|
|
182
|
+
try:
|
|
183
|
+
result = subprocess.run(
|
|
184
|
+
["git", "log", "-1", "--format=%H", "--", file_path],
|
|
185
|
+
cwd=self.project_path,
|
|
186
|
+
capture_output=True,
|
|
187
|
+
text=True,
|
|
188
|
+
timeout=5
|
|
189
|
+
)
|
|
190
|
+
if result.returncode == 0:
|
|
191
|
+
return result.stdout.strip()
|
|
192
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
193
|
+
pass
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def get_commit_info(self, commit_hash: str = "HEAD") -> Dict[str, Any]:
|
|
197
|
+
"""获取提交信息。"""
|
|
198
|
+
try:
|
|
199
|
+
result = subprocess.run(
|
|
200
|
+
["git", "log", "-1", "--format=%H%n%an%n%ae%n%ad%n%s", "--date=iso",
|
|
201
|
+
commit_hash],
|
|
202
|
+
cwd=self.project_path,
|
|
203
|
+
capture_output=True,
|
|
204
|
+
text=True,
|
|
205
|
+
timeout=5
|
|
206
|
+
)
|
|
207
|
+
if result.returncode == 0:
|
|
208
|
+
lines = result.stdout.strip().split('\n')
|
|
209
|
+
return {
|
|
210
|
+
"hash": lines[0] if len(lines) > 0 else "",
|
|
211
|
+
"author": lines[1] if len(lines) > 1 else "",
|
|
212
|
+
"email": lines[2] if len(lines) > 2 else "",
|
|
213
|
+
"date": lines[3] if len(lines) > 3 else "",
|
|
214
|
+
"message": '\n'.join(lines[4:]) if len(lines) > 4 else ""
|
|
215
|
+
}
|
|
216
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
217
|
+
pass
|
|
218
|
+
return {}
|
|
219
|
+
|
|
220
|
+
def _run_git_pull(self) -> Tuple[bool, Optional[Exception]]:
|
|
221
|
+
"""执行 git pull。"""
|
|
222
|
+
try:
|
|
223
|
+
result = subprocess.run(
|
|
224
|
+
["git", "pull", "--ff-only"],
|
|
225
|
+
cwd=self.project_path,
|
|
226
|
+
capture_output=True,
|
|
227
|
+
text=True,
|
|
228
|
+
timeout=30
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if result.returncode == 0:
|
|
232
|
+
logger.info(f"Git pull 成功: {result.stdout}")
|
|
233
|
+
return True, None
|
|
234
|
+
else:
|
|
235
|
+
logger.error(f"Git pull 失败: {result.stderr}")
|
|
236
|
+
return False, Exception(result.stderr)
|
|
237
|
+
|
|
238
|
+
except subprocess.TimeoutExpired:
|
|
239
|
+
error = Exception("Git pull 超时")
|
|
240
|
+
logger.error(str(error))
|
|
241
|
+
return False, error
|
|
242
|
+
|
|
243
|
+
def _read_local_file(self, file_path: str) -> str:
|
|
244
|
+
"""读取本地文件内容。"""
|
|
245
|
+
try:
|
|
246
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
247
|
+
return f.read()
|
|
248
|
+
except (IOError, UnicodeDecodeError) as e:
|
|
249
|
+
logger.warning(f"读取文件失败: {file_path}, {e}")
|
|
250
|
+
return ""
|
|
251
|
+
|
|
252
|
+
def _git_show(self, file_path: str) -> Optional[str]:
|
|
253
|
+
"""从 Git 获取文件内容。"""
|
|
254
|
+
try:
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
["git", "show", file_path],
|
|
257
|
+
cwd=self.project_path,
|
|
258
|
+
capture_output=True,
|
|
259
|
+
text=True,
|
|
260
|
+
timeout=10
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if result.returncode == 0:
|
|
264
|
+
return result.stdout
|
|
265
|
+
else:
|
|
266
|
+
logger.debug(f"Git show 失败: {result.stderr}")
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
270
|
+
logger.warning(f"Git show 超时: {file_path}")
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class GitConfigChecker:
|
|
275
|
+
"""Git 配置检查器。"""
|
|
276
|
+
|
|
277
|
+
def __init__(self, project_path: str = "."):
|
|
278
|
+
self.project_path = Path(project_path)
|
|
279
|
+
|
|
280
|
+
def check_git_installed(self) -> bool:
|
|
281
|
+
"""检查 Git 是否安装。"""
|
|
282
|
+
try:
|
|
283
|
+
subprocess.run(
|
|
284
|
+
["git", "--version"],
|
|
285
|
+
capture_output=True,
|
|
286
|
+
timeout=5
|
|
287
|
+
)
|
|
288
|
+
return True
|
|
289
|
+
except FileNotFoundError:
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
def check_git_configured(self) -> Tuple[bool, str]:
|
|
293
|
+
"""检查 Git 是否已配置。"""
|
|
294
|
+
try:
|
|
295
|
+
result = subprocess.run(
|
|
296
|
+
["git", "config", "--global", "--list"],
|
|
297
|
+
capture_output=True,
|
|
298
|
+
text=True,
|
|
299
|
+
timeout=5
|
|
300
|
+
)
|
|
301
|
+
if "user.name" in result.stdout and "user.email" in result.stdout:
|
|
302
|
+
return True, "Git 已配置用户信息"
|
|
303
|
+
else:
|
|
304
|
+
return False, "Git 未配置用户信息 (user.name/user.email)"
|
|
305
|
+
except FileNotFoundError:
|
|
306
|
+
return False, "Git 未安装"
|
|
307
|
+
|
|
308
|
+
def check_remote_configured(self) -> Tuple[bool, str]:
|
|
309
|
+
"""检查远程仓库是否配置。"""
|
|
310
|
+
try:
|
|
311
|
+
result = subprocess.run(
|
|
312
|
+
["git", "remote", "-v"],
|
|
313
|
+
cwd=self.project_path,
|
|
314
|
+
capture_output=True,
|
|
315
|
+
text=True,
|
|
316
|
+
timeout=5
|
|
317
|
+
)
|
|
318
|
+
if result.stdout.strip():
|
|
319
|
+
return True, f"远程仓库已配置: {result.stdout.strip()}"
|
|
320
|
+
else:
|
|
321
|
+
return False, "未配置远程仓库"
|
|
322
|
+
except subprocess.TimeoutExpired:
|
|
323
|
+
return False, "Git 命令超时"
|
|
324
|
+
|
|
325
|
+
def get_full_status(self) -> Dict[str, Any]:
|
|
326
|
+
"""获取完整的 Git 状态。"""
|
|
327
|
+
enforcer = GitWorkflowEnforcer(str(self.project_path))
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
"git_installed": self.check_git_installed(),
|
|
331
|
+
"git_configured": self.check_git_configured()[0],
|
|
332
|
+
"is_repository": enforcer.is_git_repository(),
|
|
333
|
+
"remote_configured": self.check_remote_configured()[0],
|
|
334
|
+
"git_root": str(enforcer.get_git_root()) if enforcer.get_git_root() else None
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
logging.basicConfig(level=logging.INFO)
|
|
340
|
+
|
|
341
|
+
checker = GitConfigChecker(".")
|
|
342
|
+
print("Git 配置检查:")
|
|
343
|
+
status = checker.get_full_status()
|
|
344
|
+
for key, value in status.items():
|
|
345
|
+
print(f" {key}: {value}")
|
|
346
|
+
|
|
347
|
+
print("\nGit 工作流强制执行:")
|
|
348
|
+
enforcer = GitWorkflowEnforcer(".")
|
|
349
|
+
is_clean, violation = enforcer.verify_git_status("test_agent")
|
|
350
|
+
print(f" 工作区干净: {is_clean}")
|
|
351
|
+
if violation:
|
|
352
|
+
print(f" 违规: {violation.reason}")
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""迭代状态管理模块。
|
|
2
|
+
|
|
3
|
+
功能:
|
|
4
|
+
1. 管理多迭代状态隔离
|
|
5
|
+
2. 验证迭代状态与实际进度一致
|
|
6
|
+
3. 重置指定阶段状态
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Optional, List
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IterationStatus(Enum):
|
|
20
|
+
"""迭代状态。"""
|
|
21
|
+
PENDING = "pending"
|
|
22
|
+
IN_PROGRESS = "in_progress"
|
|
23
|
+
COMPLETED = "completed"
|
|
24
|
+
ARCHIVED = "archived"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PhaseStatus(Enum):
|
|
28
|
+
"""阶段状态。"""
|
|
29
|
+
PENDING = "pending"
|
|
30
|
+
IN_PROGRESS = "in_progress"
|
|
31
|
+
COMPLETED = "completed"
|
|
32
|
+
APPROVED = "approved"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class IterationState:
|
|
37
|
+
"""迭代状态。"""
|
|
38
|
+
version: str
|
|
39
|
+
status: str = IterationStatus.PENDING.value
|
|
40
|
+
start_date: str = ""
|
|
41
|
+
end_date: str = ""
|
|
42
|
+
requirements: str = PhaseStatus.PENDING.value
|
|
43
|
+
design: str = PhaseStatus.PENDING.value
|
|
44
|
+
development: str = PhaseStatus.PENDING.value
|
|
45
|
+
testing: str = PhaseStatus.PENDING.value
|
|
46
|
+
deployment: str = PhaseStatus.PENDING.value
|
|
47
|
+
history: List[Dict] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> Dict:
|
|
50
|
+
return {
|
|
51
|
+
"version": self.version,
|
|
52
|
+
"status": self.status,
|
|
53
|
+
"start_date": self.start_date,
|
|
54
|
+
"end_date": self.end_date,
|
|
55
|
+
"requirements": self.requirements,
|
|
56
|
+
"design": self.design,
|
|
57
|
+
"development": self.development,
|
|
58
|
+
"testing": self.testing,
|
|
59
|
+
"deployment": self.deployment,
|
|
60
|
+
"history": self.history
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class IterationStatusManager:
|
|
65
|
+
"""迭代状态管理器。"""
|
|
66
|
+
|
|
67
|
+
PHASES = ["requirements", "design", "development", "testing", "deployment"]
|
|
68
|
+
|
|
69
|
+
def __init__(self, state_path: str):
|
|
70
|
+
"""初始化。
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
state_path: state.yaml 文件路径
|
|
74
|
+
"""
|
|
75
|
+
self.state_path = state_path
|
|
76
|
+
self.state = self._load_state()
|
|
77
|
+
self.current_iteration = self._get_current_iteration()
|
|
78
|
+
|
|
79
|
+
def _load_state(self) -> Dict:
|
|
80
|
+
"""加载状态文件。"""
|
|
81
|
+
try:
|
|
82
|
+
with open(self.state_path, 'r', encoding='utf-8') as f:
|
|
83
|
+
return yaml.safe_load(f) or {}
|
|
84
|
+
except (IOError, yaml.YAMLError) as e:
|
|
85
|
+
logger.error(f"加载状态文件失败: {e}")
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
def _save_state(self):
|
|
89
|
+
"""保存状态文件。"""
|
|
90
|
+
try:
|
|
91
|
+
with open(self.state_path, 'w', encoding='utf-8') as f:
|
|
92
|
+
yaml.dump(self.state, f, allow_unicode=True, sort_keys=False)
|
|
93
|
+
except IOError as e:
|
|
94
|
+
logger.error(f"保存状态文件失败: {e}")
|
|
95
|
+
|
|
96
|
+
def _get_current_iteration(self) -> Optional[str]:
|
|
97
|
+
"""获取当前迭代版本。"""
|
|
98
|
+
return self.state.get("iteration", {}).get("current")
|
|
99
|
+
|
|
100
|
+
def get_iteration_state(self, version: str = None) -> Optional[IterationState]:
|
|
101
|
+
"""获取指定迭代的状态。"""
|
|
102
|
+
target_version = version or self.current_iteration
|
|
103
|
+
if not target_version:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
iterations = self.state.get("iterations", {})
|
|
107
|
+
if target_version in iterations:
|
|
108
|
+
data = iterations[target_version]
|
|
109
|
+
return IterationState(**data)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def get_current_state(self) -> Optional[IterationState]:
|
|
113
|
+
"""获取当前迭代状态。"""
|
|
114
|
+
return self.get_iteration_state()
|
|
115
|
+
|
|
116
|
+
def start_iteration(self, version: str) -> IterationState:
|
|
117
|
+
"""开始新迭代,初始化状态。"""
|
|
118
|
+
if "iterations" not in self.state:
|
|
119
|
+
self.state["iterations"] = {}
|
|
120
|
+
|
|
121
|
+
now = datetime.now().isoformat()
|
|
122
|
+
new_iteration = IterationState(
|
|
123
|
+
version=version,
|
|
124
|
+
status=IterationStatus.IN_PROGRESS.value,
|
|
125
|
+
start_date=now
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.state["iterations"][version] = new_iteration.to_dict()
|
|
129
|
+
self.state["iteration"] = {
|
|
130
|
+
"current": version,
|
|
131
|
+
"status": "in_progress"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
self.current_iteration = version
|
|
135
|
+
self._save_state()
|
|
136
|
+
|
|
137
|
+
logger.info(f"开始新迭代: {version}")
|
|
138
|
+
return new_iteration
|
|
139
|
+
|
|
140
|
+
def complete_iteration(self, version: str = None) -> bool:
|
|
141
|
+
"""完成迭代。"""
|
|
142
|
+
target_version = version or self.current_iteration
|
|
143
|
+
if not target_version:
|
|
144
|
+
logger.error("没有当前迭代")
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
now = datetime.now().isoformat()
|
|
148
|
+
if target_version in self.state.get("iterations", {}):
|
|
149
|
+
self.state["iterations"][target_version]["status"] = IterationStatus.COMPLETED.value
|
|
150
|
+
self.state["iterations"][target_version]["end_date"] = now
|
|
151
|
+
self.state["iterations"][target_version]["status"] = IterationStatus.COMPLETED.value
|
|
152
|
+
self.state["iteration"]["status"] = "completed"
|
|
153
|
+
|
|
154
|
+
self._save_state()
|
|
155
|
+
logger.info(f"迭代已完成: {target_version}")
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def update_phase_status(self, phase: str, status: str, version: str = None) -> bool:
|
|
161
|
+
"""更新阶段状态。"""
|
|
162
|
+
if phase not in self.PHASES:
|
|
163
|
+
logger.error(f"未知阶段: {phase}")
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
target_version = version or self.current_iteration
|
|
167
|
+
if not target_version:
|
|
168
|
+
logger.error("没有当前迭代")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
if target_version in self.state.get("iterations", {}):
|
|
172
|
+
self.state["iterations"][target_version][phase] = status
|
|
173
|
+
|
|
174
|
+
self._save_state()
|
|
175
|
+
logger.info(f"迭代 {target_version} 的 {phase} 状态已更新为 {status}")
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def reset_phase(self, phase: str, version: str = None) -> bool:
|
|
181
|
+
"""重置指定阶段状态为 pending。"""
|
|
182
|
+
return self.update_phase_status(phase, PhaseStatus.PENDING.value, version)
|
|
183
|
+
|
|
184
|
+
def archive_iteration(self, version: str) -> bool:
|
|
185
|
+
"""归档迭代。"""
|
|
186
|
+
if version in self.state.get("iterations", {}):
|
|
187
|
+
self.state["iterations"][version]["status"] = IterationStatus.ARCHIVED.value
|
|
188
|
+
|
|
189
|
+
if self.state.get("iteration", {}).get("current") == version:
|
|
190
|
+
self.state["iteration"]["current"] = None
|
|
191
|
+
self.state["iteration"]["status"] = "archived"
|
|
192
|
+
|
|
193
|
+
self._save_state()
|
|
194
|
+
logger.info(f"迭代已归档: {version}")
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
def switch_iteration(self, version: str) -> bool:
|
|
200
|
+
"""切换到指定迭代。"""
|
|
201
|
+
if version in self.state.get("iterations", {}):
|
|
202
|
+
self.state["iteration"]["current"] = version
|
|
203
|
+
self.state["iteration"]["status"] = self.state["iterations"][version]["status"]
|
|
204
|
+
self.current_iteration = version
|
|
205
|
+
|
|
206
|
+
self._save_state()
|
|
207
|
+
logger.info(f"已切换到迭代: {version}")
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
logger.error(f"迭代不存在: {version}")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def get_progress(self, version: str = None) -> Dict:
|
|
214
|
+
"""获取迭代进度。"""
|
|
215
|
+
target_version = version or self.current_iteration
|
|
216
|
+
if not target_version:
|
|
217
|
+
return {"error": "没有当前迭代"}
|
|
218
|
+
|
|
219
|
+
iteration = self.get_iteration_state(target_version)
|
|
220
|
+
if not iteration:
|
|
221
|
+
return {"error": f"迭代不存在: {target_version}"}
|
|
222
|
+
|
|
223
|
+
completed_phases = 0
|
|
224
|
+
total_phases = len(self.PHASES)
|
|
225
|
+
|
|
226
|
+
for phase in self.PHASES:
|
|
227
|
+
status = getattr(iteration, phase)
|
|
228
|
+
if status in [PhaseStatus.COMPLETED.value, PhaseStatus.APPROVED.value]:
|
|
229
|
+
completed_phases += 1
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"version": target_version,
|
|
233
|
+
"overall_status": iteration.status,
|
|
234
|
+
"phases": {
|
|
235
|
+
phase: getattr(iteration, phase) for phase in self.PHASES
|
|
236
|
+
},
|
|
237
|
+
"progress_percent": (completed_phases / total_phases) * 100,
|
|
238
|
+
"completed_phases": completed_phases,
|
|
239
|
+
"total_phases": total_phases
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
def get_all_iterations(self) -> List[Dict]:
|
|
243
|
+
"""获取所有迭代信息。"""
|
|
244
|
+
iterations = self.state.get("iterations", {})
|
|
245
|
+
return [
|
|
246
|
+
{"version": k, **v} for k, v in iterations.items()
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
def validate_consistency(self, version: str = None) -> Dict:
|
|
250
|
+
"""验证迭代状态与实际进度一致。"""
|
|
251
|
+
target_version = version or self.current_iteration
|
|
252
|
+
if not target_version:
|
|
253
|
+
return {"valid": False, "reason": "没有当前迭代"}
|
|
254
|
+
|
|
255
|
+
iteration = self.get_iteration_state(target_version)
|
|
256
|
+
if not iteration:
|
|
257
|
+
return {"valid": False, "reason": f"迭代不存在: {target_version}"}
|
|
258
|
+
|
|
259
|
+
issues = []
|
|
260
|
+
|
|
261
|
+
for phase in self.PHASES:
|
|
262
|
+
status = getattr(iteration, phase)
|
|
263
|
+
|
|
264
|
+
if status == PhaseStatus.IN_PROGRESS.value:
|
|
265
|
+
issues.append({
|
|
266
|
+
"phase": phase,
|
|
267
|
+
"status": status,
|
|
268
|
+
"issue": "阶段状态为进行中,请确保工作已完成后再更新状态"
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
"valid": len(issues) == 0,
|
|
273
|
+
"version": target_version,
|
|
274
|
+
"issues": issues
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
logging.basicConfig(level=logging.INFO)
|
|
280
|
+
|
|
281
|
+
manager = IterationStatusManager("state/project_state.yaml")
|
|
282
|
+
|
|
283
|
+
print("当前迭代状态:")
|
|
284
|
+
print(manager.get_current_state())
|
|
285
|
+
|
|
286
|
+
print("\n进度:")
|
|
287
|
+
print(manager.get_progress())
|
|
288
|
+
|
|
289
|
+
print("\n一致性验证:")
|
|
290
|
+
print(manager.validate_consistency())
|