opencode-collaboration 2.1.0__py3-none-any.whl → 2.2.0.post1__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.
@@ -0,0 +1,712 @@
1
+ """用户故事管理模块 - v2.2.0 M4 用户故事管理
2
+
3
+ 提供用户故事创建、列表、详情、关联测试、验收确认等功能。
4
+ """
5
+ import uuid
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+ import yaml
11
+ import logging
12
+ from datetime import datetime
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class StoryStatus(Enum):
19
+ """用户故事状态枚举。"""
20
+ DRAFT = "draft" # 草稿
21
+ IN_PROGRESS = "in_progress" # 进行中
22
+ PENDING_ACCEPTANCE = "pending_acceptance" # 待验收
23
+ ACCEPTED = "accepted" # 已验收
24
+ ARCHIVED = "archived" # 已归档
25
+
26
+
27
+ class AcceptanceCriteriaStatus(Enum):
28
+ """验收标准状态枚举。"""
29
+ PENDING = "pending" # 待验收
30
+ PASSED = "passed" # 通过
31
+ FAILED = "failed" # 失败
32
+
33
+
34
+ @dataclass
35
+ class InteractionStep:
36
+ """交互步骤。"""
37
+ step_number: int
38
+ user_action: str
39
+ system_response: str
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ return {
43
+ "step_number": self.step_number,
44
+ "user_action": self.user_action,
45
+ "system_response": self.system_response
46
+ }
47
+
48
+ @classmethod
49
+ def from_dict(cls, data: Dict[str, Any]) -> "InteractionStep":
50
+ return cls(
51
+ step_number=data["step_number"],
52
+ user_action=data["user_action"],
53
+ system_response=data["system_response"]
54
+ )
55
+
56
+
57
+ @dataclass
58
+ class AcceptanceCriteria:
59
+ """验收标准。"""
60
+ criteria_id: str
61
+ description: str
62
+ status: AcceptanceCriteriaStatus = AcceptanceCriteriaStatus.PENDING
63
+ notes: str = ""
64
+
65
+ def to_dict(self) -> Dict[str, Any]:
66
+ return {
67
+ "criteria_id": self.criteria_id,
68
+ "description": self.description,
69
+ "status": self.status.value,
70
+ "notes": self.notes
71
+ }
72
+
73
+ @classmethod
74
+ def from_dict(cls, data: Dict[str, Any]) -> "AcceptanceCriteria":
75
+ return cls(
76
+ criteria_id=data["criteria_id"],
77
+ description=data["description"],
78
+ status=AcceptanceCriteriaStatus(data.get("status", "pending")),
79
+ notes=data.get("notes", "")
80
+ )
81
+
82
+
83
+ @dataclass
84
+ class E2ETestCoverage:
85
+ """E2E 测试覆盖。"""
86
+ test_file: str
87
+ test_case: str
88
+ description: str
89
+ status: str = "pending" # pending, passed, failed
90
+
91
+ def to_dict(self) -> Dict[str, Any]:
92
+ return {
93
+ "test_file": self.test_file,
94
+ "test_case": self.test_case,
95
+ "description": self.description,
96
+ "status": self.status
97
+ }
98
+
99
+ @classmethod
100
+ def from_dict(cls, data: Dict[str, Any]) -> "E2ETestCoverage":
101
+ return cls(
102
+ test_file=data["test_file"],
103
+ test_case=data["test_case"],
104
+ description=data["description"],
105
+ status=data.get("status", "pending")
106
+ )
107
+
108
+
109
+ @dataclass
110
+ class ExpectedResult:
111
+ """预期结果。"""
112
+ scenario_type: str # success, failure
113
+ description: str
114
+ handling: Optional[str] = None # For failure scenarios
115
+
116
+ def to_dict(self) -> Dict[str, Any]:
117
+ return {
118
+ "scenario_type": self.scenario_type,
119
+ "description": self.description,
120
+ "handling": self.handling
121
+ }
122
+
123
+ @classmethod
124
+ def from_dict(cls, data: Dict[str, Any]) -> "ExpectedResult":
125
+ return cls(
126
+ scenario_type=data["scenario_type"],
127
+ description=data["description"],
128
+ handling=data.get("handling")
129
+ )
130
+
131
+
132
+ @dataclass
133
+ class UserStory:
134
+ """用户故事。"""
135
+ story_id: str
136
+ title: str
137
+ role: str # 用户角色
138
+ goal: str # 希望完成什么
139
+ value: str # 以便获得什么价值
140
+ preconditions: List[str] = field(default_factory=list)
141
+ interaction_steps: List[InteractionStep] = field(default_factory=list)
142
+ expected_results: List[ExpectedResult] = field(default_factory=list)
143
+ e2e_tests: List[E2ETestCoverage] = field(default_factory=list)
144
+ acceptance_criteria: List[AcceptanceCriteria] = field(default_factory=list)
145
+ status: StoryStatus = StoryStatus.DRAFT
146
+ version: str = "v2.2.0"
147
+ evidence: Optional[str] = None # 验收证据
148
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
149
+ updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
150
+
151
+ def to_dict(self) -> Dict[str, Any]:
152
+ return {
153
+ "story_id": self.story_id,
154
+ "title": self.title,
155
+ "role": self.role,
156
+ "goal": self.goal,
157
+ "value": self.value,
158
+ "preconditions": self.preconditions,
159
+ "interaction_steps": [s.to_dict() for s in self.interaction_steps],
160
+ "expected_results": [r.to_dict() for r in self.expected_results],
161
+ "e2e_tests": [t.to_dict() for t in self.e2e_tests],
162
+ "acceptance_criteria": [c.to_dict() for c in self.acceptance_criteria],
163
+ "status": self.status.value,
164
+ "version": self.version,
165
+ "evidence": self.evidence,
166
+ "created_at": self.created_at,
167
+ "updated_at": self.updated_at
168
+ }
169
+
170
+ @classmethod
171
+ def from_dict(cls, data: Dict[str, Any]) -> "UserStory":
172
+ return cls(
173
+ story_id=data["story_id"],
174
+ title=data["title"],
175
+ role=data["role"],
176
+ goal=data["goal"],
177
+ value=data["value"],
178
+ preconditions=data.get("preconditions", []),
179
+ interaction_steps=[
180
+ InteractionStep.from_dict(s) for s in data.get("interaction_steps", [])
181
+ ],
182
+ expected_results=[
183
+ ExpectedResult.from_dict(r) for r in data.get("expected_results", [])
184
+ ],
185
+ e2e_tests=[
186
+ E2ETestCoverage.from_dict(t) for t in data.get("e2e_tests", [])
187
+ ],
188
+ acceptance_criteria=[
189
+ AcceptanceCriteria.from_dict(c) for c in data.get("acceptance_criteria", [])
190
+ ],
191
+ status=StoryStatus(data.get("status", "draft")),
192
+ version=data.get("version", "v2.2.0"),
193
+ evidence=data.get("evidence"),
194
+ created_at=data.get("created_at", datetime.now().isoformat()),
195
+ updated_at=data.get("updated_at", datetime.now().isoformat())
196
+ )
197
+
198
+ def to_markdown(self) -> str:
199
+ """转换为 Markdown 格式。"""
200
+ lines = [
201
+ f"## Story {self.story_id}: {self.title}",
202
+ "",
203
+ "### 用户目标",
204
+ f"**作为** {self.role}",
205
+ f"**我希望** {self.goal}",
206
+ f"**以便** {self.value}",
207
+ ""
208
+ ]
209
+
210
+ if self.preconditions:
211
+ lines.extend(["### 前置条件", ""])
212
+ for precondition in self.preconditions:
213
+ lines.append(f"- {precondition}")
214
+ lines.append("")
215
+
216
+ if self.interaction_steps:
217
+ lines.extend(["### 交互流程", ""])
218
+ lines.append("| 步骤 | 用户操作 | 系统响应 |")
219
+ lines.append("|------|----------|----------|")
220
+ for step in self.interaction_steps:
221
+ lines.append(f"| {step.step_number} | {step.user_action} | {step.system_response} |")
222
+ lines.append("")
223
+
224
+ if self.expected_results:
225
+ lines.append("### 预期结果")
226
+ for result in self.expected_results:
227
+ if result.scenario_type == "success":
228
+ lines.append("**成功场景**:")
229
+ else:
230
+ lines.append("**失败场景**:")
231
+ lines.append(f"- {result.description}")
232
+ if result.handling:
233
+ lines.append(f" - 处理方式: {result.handling}")
234
+ lines.append("")
235
+
236
+ if self.e2e_tests:
237
+ lines.extend(["### E2E 测试覆盖", ""])
238
+ lines.append("| 测试用例 | 说明 |")
239
+ lines.append("|----------|------|")
240
+ for test in self.e2e_tests:
241
+ lines.append(f"| {test.test_case} | {test.description} |")
242
+ lines.append("")
243
+
244
+ if self.acceptance_criteria:
245
+ lines.extend(["### 验收标准", ""])
246
+ for criteria in self.acceptance_criteria:
247
+ checkbox = "[x]" if criteria.status == AcceptanceCriteriaStatus.PASSED else "[ ]"
248
+ lines.append(f"- {checkbox} {criteria.description}")
249
+ lines.append("")
250
+
251
+ return "\n".join(lines)
252
+
253
+
254
+ class StoryManagerError(Exception):
255
+ """用户故事管理异常基类。"""
256
+ pass
257
+
258
+
259
+ class StoryNotFoundError(StoryManagerError):
260
+ """用户故事未找到异常。"""
261
+ pass
262
+
263
+
264
+ class StoryManager:
265
+ """用户故事管理器。"""
266
+
267
+ STORIES_FILE = "stories.yaml"
268
+
269
+ def __init__(
270
+ self,
271
+ project_path: str,
272
+ stories_file: Optional[str] = None
273
+ ):
274
+ """初始化用户故事管理器。
275
+
276
+ Args:
277
+ project_path: 项目路径
278
+ stories_file: 故事数据文件
279
+ """
280
+ self.project_path = Path(project_path)
281
+ self.stories_file = self.project_path / (stories_file or self.STORIES_FILE)
282
+ self.stories: Dict[str, UserStory] = {}
283
+ self._ensure_directories()
284
+ self._load_stories()
285
+
286
+ def _ensure_directories(self) -> None:
287
+ """确保目录存在。"""
288
+ self.project_path.mkdir(parents=True, exist_ok=True)
289
+
290
+ def _load_stories(self) -> None:
291
+ """加载故事数据。"""
292
+ if self.stories_file.exists():
293
+ try:
294
+ with open(self.stories_file, 'r', encoding='utf-8') as f:
295
+ data = yaml.safe_load(f)
296
+ if data and "stories" in data:
297
+ for story_data in data.get("stories", []):
298
+ story = UserStory.from_dict(story_data)
299
+ self.stories[story.story_id] = story
300
+ except Exception as e:
301
+ logger.warning(f"加载用户故事数据失败: {e}")
302
+
303
+ def _save_stories(self) -> None:
304
+ """保存故事数据。"""
305
+ data = {
306
+ "stories": [s.to_dict() for s in self.stories.values()],
307
+ "updated_at": datetime.now().isoformat()
308
+ }
309
+ with open(self.stories_file, 'w', encoding='utf-8') as f:
310
+ yaml.dump(data, f, allow_unicode=True)
311
+
312
+ def create_story(
313
+ self,
314
+ title: str,
315
+ role: str,
316
+ goal: str,
317
+ value: str,
318
+ version: Optional[str] = None
319
+ ) -> UserStory:
320
+ """创建用户故事。
321
+
322
+ Args:
323
+ title: 故事标题
324
+ role: 用户角色
325
+ goal: 希望完成什么
326
+ value: 以便获得什么价值
327
+ version: 关联版本
328
+
329
+ Returns:
330
+ 创建的用户故事
331
+ """
332
+ story_id = f"S-{len(self.stories) + 1:03d}"
333
+ story = UserStory(
334
+ story_id=story_id,
335
+ title=title,
336
+ role=role,
337
+ goal=goal,
338
+ value=value,
339
+ version=version or "v2.2.0"
340
+ )
341
+ self.stories[story_id] = story
342
+ self._save_stories()
343
+ logger.info(f"创建用户故事: {story_id} - {title}")
344
+ return story
345
+
346
+ def get_story(self, story_id: str) -> UserStory:
347
+ """获取用户故事。
348
+
349
+ Args:
350
+ story_id: 故事 ID
351
+
352
+ Returns:
353
+ 用户故事
354
+
355
+ Raises:
356
+ StoryNotFoundError: 故事未找到
357
+ """
358
+ if story_id not in self.stories:
359
+ raise StoryNotFoundError(f"用户故事未找到: {story_id}")
360
+ return self.stories[story_id]
361
+
362
+ def list_stories(
363
+ self,
364
+ status: Optional[StoryStatus] = None,
365
+ version: Optional[str] = None
366
+ ) -> List[UserStory]:
367
+ """列出用户故事。
368
+
369
+ Args:
370
+ status: 状态过滤
371
+ version: 版本过滤
372
+
373
+ Returns:
374
+ 用户故事列表
375
+ """
376
+ stories = list(self.stories.values())
377
+
378
+ if status:
379
+ stories = [s for s in stories if s.status == status]
380
+ if version:
381
+ stories = [s for s in stories if s.version == version]
382
+
383
+ return sorted(stories, key=lambda s: s.story_id)
384
+
385
+ def add_precondition(self, story_id: str, precondition: str) -> UserStory:
386
+ """添加前置条件。
387
+
388
+ Args:
389
+ story_id: 故事 ID
390
+ precondition: 前置条件
391
+
392
+ Returns:
393
+ 更新后的故事
394
+
395
+ Raises:
396
+ StoryNotFoundError: 故事未找到
397
+ """
398
+ story = self.get_story(story_id)
399
+ story.preconditions.append(precondition)
400
+ story.updated_at = datetime.now().isoformat()
401
+ self._save_stories()
402
+ logger.info(f"添加前置条件: {story_id} - {precondition[:50]}...")
403
+ return story
404
+
405
+ def add_interaction_step(
406
+ self,
407
+ story_id: str,
408
+ user_action: str,
409
+ system_response: str
410
+ ) -> UserStory:
411
+ """添加交互步骤。
412
+
413
+ Args:
414
+ story_id: 故事 ID
415
+ user_action: 用户操作
416
+ system_response: 系统响应
417
+
418
+ Returns:
419
+ 更新后的故事
420
+
421
+ Raises:
422
+ StoryNotFoundError: 故事未找到
423
+ """
424
+ story = self.get_story(story_id)
425
+ step = InteractionStep(
426
+ step_number=len(story.interaction_steps) + 1,
427
+ user_action=user_action,
428
+ system_response=system_response
429
+ )
430
+ story.interaction_steps.append(step)
431
+ story.updated_at = datetime.now().isoformat()
432
+ self._save_stories()
433
+ logger.info(f"添加交互步骤: {story_id} - 步骤 {step.step_number}")
434
+ return story
435
+
436
+ def add_expected_result(
437
+ self,
438
+ story_id: str,
439
+ scenario_type: str,
440
+ description: str,
441
+ handling: Optional[str] = None
442
+ ) -> UserStory:
443
+ """添加预期结果。
444
+
445
+ Args:
446
+ story_id: 故事 ID
447
+ scenario_type: 场景类型 (success/failure)
448
+ description: 描述
449
+ handling: 处理方式(失败场景)
450
+
451
+ Returns:
452
+ 更新后的故事
453
+
454
+ Raises:
455
+ StoryNotFoundError: 故事未找到
456
+ """
457
+ story = self.get_story(story_id)
458
+ result = ExpectedResult(
459
+ scenario_type=scenario_type,
460
+ description=description,
461
+ handling=handling
462
+ )
463
+ story.expected_results.append(result)
464
+ story.updated_at = datetime.now().isoformat()
465
+ self._save_stories()
466
+ logger.info(f"添加预期结果: {story_id} - {scenario_type}")
467
+ return story
468
+
469
+ def link_e2e_test(
470
+ self,
471
+ story_id: str,
472
+ test_file: str,
473
+ test_case: str,
474
+ description: str
475
+ ) -> UserStory:
476
+ """关联 E2E 测试用例。
477
+
478
+ Args:
479
+ story_id: 故事 ID
480
+ test_file: 测试文件
481
+ test_case: 测试用例名
482
+ description: 说明
483
+
484
+ Returns:
485
+ 更新后的故事
486
+
487
+ Raises:
488
+ StoryNotFoundError: 故事未找到
489
+ """
490
+ story = self.get_story(story_id)
491
+ test = E2ETestCoverage(
492
+ test_file=test_file,
493
+ test_case=test_case,
494
+ description=description
495
+ )
496
+ story.e2e_tests.append(test)
497
+ story.updated_at = datetime.now().isoformat()
498
+ self._save_stories()
499
+ logger.info(f"关联E2E测试: {story_id} - {test_case}")
500
+ return story
501
+
502
+ def add_acceptance_criteria(
503
+ self,
504
+ story_id: str,
505
+ criteria_id: str,
506
+ description: str
507
+ ) -> UserStory:
508
+ """添加验收标准。
509
+
510
+ Args:
511
+ story_id: 故事 ID
512
+ criteria_id: 标准 ID
513
+ description: 标准描述
514
+
515
+ Returns:
516
+ 更新后的故事
517
+
518
+ Raises:
519
+ StoryNotFoundError: 故事未找到
520
+ """
521
+ story = self.get_story(story_id)
522
+ criteria = AcceptanceCriteria(
523
+ criteria_id=criteria_id,
524
+ description=description
525
+ )
526
+ story.acceptance_criteria.append(criteria)
527
+ story.updated_at = datetime.now().isoformat()
528
+ self._save_stories()
529
+ logger.info(f"添加验收标准: {story_id} - {criteria_id}")
530
+ return story
531
+
532
+ def update_acceptance_status(
533
+ self,
534
+ story_id: str,
535
+ criteria_id: str,
536
+ status: AcceptanceCriteriaStatus,
537
+ notes: Optional[str] = None
538
+ ) -> UserStory:
539
+ """更新验收标准状态。
540
+
541
+ Args:
542
+ story_id: 故事 ID
543
+ criteria_id: 标准 ID
544
+ status: 新状态
545
+ notes: 备注
546
+
547
+ Returns:
548
+ 更新后的故事
549
+
550
+ Raises:
551
+ StoryNotFoundError: 故事未找到
552
+ """
553
+ story = self.get_story(story_id)
554
+ for criteria in story.acceptance_criteria:
555
+ if criteria.criteria_id == criteria_id:
556
+ criteria.status = status
557
+ if notes:
558
+ criteria.notes = notes
559
+ break
560
+ story.updated_at = datetime.now().isoformat()
561
+ self._save_stories()
562
+ logger.info(f"更新验收状态: {story_id} - {criteria_id} -> {status.value}")
563
+ return story
564
+
565
+ def accept_story(self, story_id: str, evidence: str) -> UserStory:
566
+ """标记故事验收通过。
567
+
568
+ Args:
569
+ story_id: 故事 ID
570
+ evidence: 验收证据
571
+
572
+ Returns:
573
+ 更新后的故事
574
+
575
+ Raises:
576
+ StoryNotFoundError: 故事未找到
577
+ """
578
+ story = self.get_story(story_id)
579
+ story.status = StoryStatus.ACCEPTED
580
+ story.evidence = evidence
581
+ story.updated_at = datetime.now().isoformat()
582
+ self._save_stories()
583
+ logger.info(f"验收通过: {story_id}")
584
+ return story
585
+
586
+ def update_story_status(
587
+ self,
588
+ story_id: str,
589
+ status: StoryStatus
590
+ ) -> UserStory:
591
+ """更新故事状态。
592
+
593
+ Args:
594
+ story_id: 故事 ID
595
+ status: 新状态
596
+
597
+ Returns:
598
+ 更新后的故事
599
+
600
+ Raises:
601
+ StoryNotFoundError: 故事未找到
602
+ """
603
+ story = self.get_story(story_id)
604
+ story.status = status
605
+ story.updated_at = datetime.now().isoformat()
606
+ self._save_stories()
607
+ logger.info(f"更新状态: {story_id} -> {status.value}")
608
+ return story
609
+
610
+ def get_story_summary(self) -> Dict[str, Any]:
611
+ """获取故事管理摘要。
612
+
613
+ Returns:
614
+ 摘要信息
615
+ """
616
+ by_status = {}
617
+ by_version = {}
618
+
619
+ for story in self.stories.values():
620
+ by_status[story.status.value] = by_status.get(story.status.value, 0) + 1
621
+ by_version[story.version] = by_version.get(story.version, 0) + 1
622
+
623
+ return {
624
+ "total_stories": len(self.stories),
625
+ "by_status": by_status,
626
+ "by_version": by_version,
627
+ "accepted_count": len([s for s in self.stories.values() if s.status == StoryStatus.ACCEPTED]),
628
+ "pending_count": len([s for s in self.stories.values() if s.status == StoryStatus.PENDING_ACCEPTANCE])
629
+ }
630
+
631
+ def export_stories(self, output_path: Optional[str] = None) -> Dict[str, Any]:
632
+ """导出故事数据。
633
+
634
+ Args:
635
+ output_path: 输出路径(可选)
636
+
637
+ Returns:
638
+ 故事数据字典
639
+ """
640
+ data = {
641
+ "stories": [s.to_dict() for s in self.stories.values()],
642
+ "summary": self.get_story_summary(),
643
+ "exported_at": datetime.now().isoformat()
644
+ }
645
+
646
+ if output_path:
647
+ with open(output_path, 'w', encoding='utf-8') as f:
648
+ yaml.dump(data, f, allow_unicode=True)
649
+
650
+ return data
651
+
652
+
653
+ if __name__ == "__main__":
654
+ import tempfile
655
+
656
+ with tempfile.TemporaryDirectory() as tmpdir:
657
+ manager = StoryManager(tmpdir)
658
+
659
+ story = manager.create_story(
660
+ title="用户登录",
661
+ role="终端用户",
662
+ goal="使用用户名密码登录系统",
663
+ value="保护账户安全"
664
+ )
665
+ print(f"创建用户故事: {story.story_id}")
666
+
667
+ manager.add_precondition(story.story_id, "用户已注册账户")
668
+ manager.add_precondition(story.story_id, "用户知道正确的用户名和密码")
669
+
670
+ manager.add_interaction_step(
671
+ story.story_id,
672
+ "输入用户名和密码",
673
+ "系统验证凭据"
674
+ )
675
+ manager.add_interaction_step(
676
+ story.story_id,
677
+ "点击登录按钮",
678
+ "系统重定向到主页"
679
+ )
680
+
681
+ manager.add_expected_result(
682
+ story.story_id,
683
+ "success",
684
+ "用户成功登录,显示欢迎信息"
685
+ )
686
+ manager.add_expected_result(
687
+ story.story_id,
688
+ "failure",
689
+ "用户名或密码错误",
690
+ handling="显示错误提示,不跳转页面"
691
+ )
692
+
693
+ manager.link_e2e_test(
694
+ story.story_id,
695
+ "test_stories/test_story_S001.py",
696
+ "test_login_success",
697
+ "测试用户登录成功场景"
698
+ )
699
+ manager.link_e2e_test(
700
+ story.story_id,
701
+ "test_stories/test_story_S001.py",
702
+ "test_login_failure",
703
+ "测试用户登录失败场景"
704
+ )
705
+
706
+ manager.add_acceptance_criteria(story.story_id, "AC-001", "使用有效凭据可成功登录")
707
+ manager.add_acceptance_criteria(story.story_id, "AC-002", "使用无效凭据显示错误提示")
708
+
709
+ print(f"\n用户故事 Markdown:\n{story.to_markdown()}")
710
+
711
+ summary = manager.get_story_summary()
712
+ print(f"\n摘要: {summary}")