monoco-toolkit 0.2.5__py3-none-any.whl → 0.2.7__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.
- monoco/core/agent/adapters.py +24 -1
- monoco/core/config.py +77 -17
- monoco/core/integrations.py +8 -0
- monoco/core/lsp.py +7 -0
- monoco/core/output.py +8 -1
- monoco/core/resources/zh/SKILL.md +6 -7
- monoco/core/setup.py +8 -0
- monoco/features/i18n/resources/zh/SKILL.md +5 -5
- monoco/features/issue/commands.py +135 -55
- monoco/features/issue/core.py +157 -122
- monoco/features/issue/domain/__init__.py +0 -0
- monoco/features/issue/domain/lifecycle.py +126 -0
- monoco/features/issue/domain/models.py +170 -0
- monoco/features/issue/domain/parser.py +223 -0
- monoco/features/issue/domain/workspace.py +104 -0
- monoco/features/issue/engine/__init__.py +22 -0
- monoco/features/issue/engine/config.py +172 -0
- monoco/features/issue/engine/machine.py +185 -0
- monoco/features/issue/engine/models.py +18 -0
- monoco/features/issue/linter.py +32 -11
- monoco/features/issue/lsp/__init__.py +3 -0
- monoco/features/issue/lsp/definition.py +72 -0
- monoco/features/issue/models.py +26 -9
- monoco/features/issue/resources/zh/SKILL.md +8 -9
- monoco/features/issue/validator.py +181 -65
- monoco/features/spike/core.py +5 -22
- monoco/features/spike/resources/zh/SKILL.md +2 -2
- monoco/main.py +2 -26
- monoco_toolkit-0.2.7.dist-info/METADATA +129 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/RECORD +33 -27
- monoco/features/agent/commands.py +0 -166
- monoco/features/agent/doctor.py +0 -30
- monoco/features/pty/core.py +0 -185
- monoco/features/pty/router.py +0 -138
- monoco/features/pty/server.py +0 -56
- monoco_toolkit-0.2.5.dist-info/METADATA +0 -93
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from typing import List, Optional, Dict
|
|
2
|
+
from monoco.core.config import IssueSchemaConfig, TransitionConfig
|
|
3
|
+
from ..models import IssueStatus, IssueStage, IssueMetadata, IssueSolution, IssueType
|
|
4
|
+
|
|
5
|
+
class StateMachine:
|
|
6
|
+
def __init__(self, config: IssueSchemaConfig):
|
|
7
|
+
self.issue_config = config
|
|
8
|
+
self.transitions = config.workflows or []
|
|
9
|
+
|
|
10
|
+
def get_type_config(self, type_name: str):
|
|
11
|
+
if not self.issue_config.types:
|
|
12
|
+
return None
|
|
13
|
+
for t in self.issue_config.types:
|
|
14
|
+
if t.name == type_name:
|
|
15
|
+
return t
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
def get_prefix_map(self) -> Dict[str, str]:
|
|
19
|
+
if not self.issue_config.types:
|
|
20
|
+
return {}
|
|
21
|
+
return {t.name: t.prefix for t in self.issue_config.types}
|
|
22
|
+
|
|
23
|
+
def get_folder_map(self) -> Dict[str, str]:
|
|
24
|
+
if not self.issue_config.types:
|
|
25
|
+
return {}
|
|
26
|
+
return {t.name: t.folder for t in self.issue_config.types}
|
|
27
|
+
|
|
28
|
+
def get_all_types(self) -> List[str]:
|
|
29
|
+
if not self.issue_config.types:
|
|
30
|
+
return []
|
|
31
|
+
return [t.name for t in self.issue_config.types]
|
|
32
|
+
|
|
33
|
+
def can_transition(self, current_status: str, current_stage: Optional[str],
|
|
34
|
+
target_status: str, target_stage: Optional[str]) -> bool:
|
|
35
|
+
"""Check if a transition path exists."""
|
|
36
|
+
for t in self.transitions:
|
|
37
|
+
if t.from_status and t.from_status != current_status:
|
|
38
|
+
continue
|
|
39
|
+
if t.from_stage and t.from_stage != current_stage:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
if t.to_status == target_status:
|
|
43
|
+
if target_stage is None or t.to_stage == target_stage:
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def get_available_transitions(self, meta: IssueMetadata) -> List[TransitionConfig]:
|
|
48
|
+
"""Get all transitions allowed from the current state of the issue."""
|
|
49
|
+
allowed = []
|
|
50
|
+
for t in self.transitions:
|
|
51
|
+
# Universal actions (no from_status/stage) are always allowed
|
|
52
|
+
if t.from_status is None and t.from_stage is None:
|
|
53
|
+
allowed.append(t)
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
# Match status
|
|
57
|
+
if t.from_status and t.from_status != meta.status:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Match stage
|
|
61
|
+
if t.from_stage and t.from_stage != meta.stage:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# Special case for 'Cancel': don't show if already DONE or CLOSED
|
|
65
|
+
if t.name == "cancel" and meta.stage == "done":
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
allowed.append(t)
|
|
69
|
+
return allowed
|
|
70
|
+
|
|
71
|
+
def find_transition(self, from_status: str, from_stage: Optional[str],
|
|
72
|
+
to_status: str, to_stage: Optional[str],
|
|
73
|
+
solution: Optional[str] = None) -> Optional[TransitionConfig]:
|
|
74
|
+
"""Find a specific transition rule."""
|
|
75
|
+
candidates = []
|
|
76
|
+
for t in self.transitions:
|
|
77
|
+
# Skip non-transitions (agent actions with same status/stage)
|
|
78
|
+
if t.from_status is None and t.from_stage is None:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if t.from_status and t.from_status != from_status:
|
|
82
|
+
continue
|
|
83
|
+
if t.from_stage and t.from_stage != from_stage:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Check if this transition matches the target
|
|
87
|
+
if t.to_status == to_status:
|
|
88
|
+
if to_stage is None or t.to_stage == to_stage:
|
|
89
|
+
candidates.append(t)
|
|
90
|
+
|
|
91
|
+
if not candidates:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# If we have a solution, find the transition that requires it
|
|
95
|
+
if solution:
|
|
96
|
+
for t in candidates:
|
|
97
|
+
if t.required_solution == solution:
|
|
98
|
+
return t
|
|
99
|
+
# If solution provided but none of the transitions match it,
|
|
100
|
+
# we should return None (unless there is a transition with NO required_solution)
|
|
101
|
+
for t in candidates:
|
|
102
|
+
if t.required_solution is None:
|
|
103
|
+
return t
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
# Otherwise return the first one that has NO required_solution
|
|
107
|
+
for t in candidates:
|
|
108
|
+
if t.required_solution is None:
|
|
109
|
+
return t
|
|
110
|
+
|
|
111
|
+
return candidates[0]
|
|
112
|
+
|
|
113
|
+
def validate_transition(self, from_status: str, from_stage: Optional[str],
|
|
114
|
+
to_status: str, to_stage: Optional[str],
|
|
115
|
+
solution: Optional[str] = None) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Validate if a transition is allowed. Raises ValueError if not.
|
|
118
|
+
"""
|
|
119
|
+
if from_status == to_status and from_stage == to_stage:
|
|
120
|
+
return # No change is always allowed (unless we want to enforce specific updates)
|
|
121
|
+
|
|
122
|
+
transition = self.find_transition(from_status, from_stage, to_status, to_stage, solution)
|
|
123
|
+
|
|
124
|
+
if not transition:
|
|
125
|
+
raise ValueError(f"Lifecycle Policy: Transition from {from_status}({from_stage if from_stage else 'None'}) "
|
|
126
|
+
f"to {to_status}({to_stage if to_stage else 'None'}) is not defined.")
|
|
127
|
+
|
|
128
|
+
if transition.required_solution and solution != transition.required_solution:
|
|
129
|
+
raise ValueError(f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'.")
|
|
130
|
+
|
|
131
|
+
def enforce_policy(self, meta: IssueMetadata) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Apply consistency rules to IssueMetadata.
|
|
134
|
+
"""
|
|
135
|
+
from ..models import current_time
|
|
136
|
+
|
|
137
|
+
if meta.status == "backlog":
|
|
138
|
+
meta.stage = "freezed"
|
|
139
|
+
|
|
140
|
+
elif meta.status == "closed":
|
|
141
|
+
if meta.stage != "done":
|
|
142
|
+
meta.stage = "done"
|
|
143
|
+
if not meta.closed_at:
|
|
144
|
+
meta.closed_at = current_time()
|
|
145
|
+
|
|
146
|
+
elif meta.status == "open":
|
|
147
|
+
if meta.stage is None:
|
|
148
|
+
meta.stage = "draft"
|
|
149
|
+
|
|
150
|
+
def validate_transition(self, from_status: str, from_stage: Optional[str],
|
|
151
|
+
to_status: str, to_stage: Optional[str],
|
|
152
|
+
solution: Optional[str] = None) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Validate if a transition is allowed. Raises ValueError if not.
|
|
155
|
+
"""
|
|
156
|
+
if from_status == to_status and from_stage == to_stage:
|
|
157
|
+
return # No change is always allowed (unless we want to enforce specific updates)
|
|
158
|
+
|
|
159
|
+
transition = self.find_transition(from_status, from_stage, to_status, to_stage, solution)
|
|
160
|
+
|
|
161
|
+
if not transition:
|
|
162
|
+
raise ValueError(f"Lifecycle Policy: Transition from {from_status}({from_stage if from_stage else 'None'}) "
|
|
163
|
+
f"to {to_status}({to_stage if to_stage else 'None'}) is not defined.")
|
|
164
|
+
|
|
165
|
+
if transition.required_solution and solution != transition.required_solution:
|
|
166
|
+
raise ValueError(f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'.")
|
|
167
|
+
|
|
168
|
+
def enforce_policy(self, meta: IssueMetadata) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Apply consistency rules to IssueMetadata.
|
|
171
|
+
"""
|
|
172
|
+
from ..models import current_time
|
|
173
|
+
|
|
174
|
+
if meta.status == "backlog":
|
|
175
|
+
meta.stage = "freezed"
|
|
176
|
+
|
|
177
|
+
elif meta.status == "closed":
|
|
178
|
+
if meta.stage != "done":
|
|
179
|
+
meta.stage = "done"
|
|
180
|
+
if not meta.closed_at:
|
|
181
|
+
meta.closed_at = current_time()
|
|
182
|
+
|
|
183
|
+
elif meta.status == "open":
|
|
184
|
+
if meta.stage is None:
|
|
185
|
+
meta.stage = "draft"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import List, Optional, Any
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
class Transition(BaseModel):
|
|
5
|
+
name: str
|
|
6
|
+
label: str
|
|
7
|
+
icon: Optional[str] = None
|
|
8
|
+
from_status: Optional[str] = None # None means any
|
|
9
|
+
from_stage: Optional[str] = None # None means any
|
|
10
|
+
to_status: str
|
|
11
|
+
to_stage: Optional[str] = None
|
|
12
|
+
required_solution: Optional[str] = None
|
|
13
|
+
description: str = ""
|
|
14
|
+
command_template: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
class StateMachineConfig(BaseModel):
|
|
17
|
+
transitions: List[Transition]
|
|
18
|
+
# We can add more config like default stages for statuses etc.
|
monoco/features/issue/linter.py
CHANGED
|
@@ -38,27 +38,48 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
38
38
|
meta = core.parse_issue(f)
|
|
39
39
|
if meta:
|
|
40
40
|
local_id = meta.id
|
|
41
|
-
full_id = f"{project_name}::{local_id}"
|
|
41
|
+
full_id = f"{project_name}::{local_id}"
|
|
42
42
|
|
|
43
43
|
all_issue_ids.add(local_id)
|
|
44
|
-
|
|
45
|
-
all_issue_ids.add(full_id)
|
|
44
|
+
all_issue_ids.add(full_id)
|
|
46
45
|
|
|
47
46
|
project_issues.append((f, meta))
|
|
48
47
|
return project_issues
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
from monoco.core.config import get_config
|
|
50
|
+
conf = get_config(str(issues_root.parent))
|
|
51
|
+
|
|
52
|
+
# Identify local project name
|
|
53
|
+
local_project_name = "local"
|
|
54
|
+
if conf and conf.project and conf.project.name:
|
|
55
|
+
local_project_name = conf.project.name.lower()
|
|
56
|
+
|
|
57
|
+
# Find Topmost Workspace Root
|
|
58
|
+
workspace_root = issues_root.parent
|
|
59
|
+
for parent in [workspace_root] + list(workspace_root.parents):
|
|
60
|
+
if (parent / ".monoco" / "workspace.yaml").exists() or (parent / ".monoco" / "project.yaml").exists():
|
|
61
|
+
workspace_root = parent
|
|
62
|
+
|
|
63
|
+
# Collect from local issues_root
|
|
64
|
+
all_issues.extend(collect_project_issues(issues_root, local_project_name))
|
|
51
65
|
|
|
52
66
|
if recursive:
|
|
53
67
|
try:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
# Re-read config from workspace root to get all members
|
|
69
|
+
ws_conf = get_config(str(workspace_root))
|
|
70
|
+
|
|
71
|
+
# Index Root project if different from current
|
|
72
|
+
if workspace_root != issues_root.parent:
|
|
73
|
+
root_issues_dir = workspace_root / "Issues"
|
|
74
|
+
if root_issues_dir.exists():
|
|
75
|
+
all_issues.extend(collect_project_issues(root_issues_dir, ws_conf.project.name.lower()))
|
|
76
|
+
|
|
77
|
+
# Index all members
|
|
78
|
+
for member_name, rel_path in ws_conf.project.members.items():
|
|
79
|
+
member_root = (workspace_root / rel_path).resolve()
|
|
59
80
|
member_issues_dir = member_root / "Issues"
|
|
60
|
-
if member_issues_dir.exists():
|
|
61
|
-
collect_project_issues(member_issues_dir, member_name)
|
|
81
|
+
if member_issues_dir.exists() and member_issues_dir != issues_root:
|
|
82
|
+
all_issues.extend(collect_project_issues(member_issues_dir, member_name.lower()))
|
|
62
83
|
except Exception:
|
|
63
84
|
pass
|
|
64
85
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from monoco.core.lsp import Location, Position, Range
|
|
4
|
+
from ..domain.parser import MarkdownParser
|
|
5
|
+
from ..domain.workspace import WorkspaceSymbolIndex, IssueLocation
|
|
6
|
+
|
|
7
|
+
class DefinitionProvider:
|
|
8
|
+
def __init__(self, workspace_root: Path):
|
|
9
|
+
self.workspace_root = workspace_root
|
|
10
|
+
self.index = WorkspaceSymbolIndex(workspace_root)
|
|
11
|
+
# Lazy indexing handled by the index class itself
|
|
12
|
+
|
|
13
|
+
def provide_definition(self, file_path: Path, position: Position) -> List[Location]:
|
|
14
|
+
"""
|
|
15
|
+
Resolve definition at the given position in the file.
|
|
16
|
+
"""
|
|
17
|
+
if not file_path.exists():
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
content = file_path.read_text()
|
|
21
|
+
|
|
22
|
+
# 1. Parse the document to find spans
|
|
23
|
+
# We only need to find the span at the specific line
|
|
24
|
+
issue = MarkdownParser.parse(content, path=str(file_path))
|
|
25
|
+
|
|
26
|
+
target_span = None
|
|
27
|
+
for block in issue.body.blocks:
|
|
28
|
+
# Check if position is within block
|
|
29
|
+
# Note: block.line_start is inclusive, line_end is exclusive for content
|
|
30
|
+
if block.line_start <= position.line < block.line_end:
|
|
31
|
+
for span in block.spans:
|
|
32
|
+
if span.range.start.line == position.line:
|
|
33
|
+
# Check character range
|
|
34
|
+
if span.range.start.character <= position.character <= span.range.end.character:
|
|
35
|
+
target_span = span
|
|
36
|
+
break
|
|
37
|
+
if target_span:
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
if not target_span:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
# 2. Resolve based on span type
|
|
44
|
+
if target_span.type in ["wikilink", "issue_id"]:
|
|
45
|
+
issue_id = target_span.metadata.get("issue_id")
|
|
46
|
+
if issue_id:
|
|
47
|
+
# Resolve using Workspace Index
|
|
48
|
+
location = self.index.resolve(issue_id, context_project=self._get_context_project(file_path))
|
|
49
|
+
if location:
|
|
50
|
+
return [
|
|
51
|
+
Location(
|
|
52
|
+
uri=f"file://{location.file_path}",
|
|
53
|
+
range=Range(
|
|
54
|
+
start=Position(line=0, character=0),
|
|
55
|
+
end=Position(line=0, character=0)
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
def _get_context_project(self, file_path: Path) -> Optional[str]:
|
|
63
|
+
# Simple heuristic: look for parent directory name if it's a known project structure?
|
|
64
|
+
# Or rely on configuration.
|
|
65
|
+
# For now, let's assume the index handles context if passed, or we pass None.
|
|
66
|
+
# Actually resolving context project from file path is tricky without config loaded for that specific root.
|
|
67
|
+
# Let's try to deduce from path relative to workspace root.
|
|
68
|
+
try:
|
|
69
|
+
rel = file_path.relative_to(self.workspace_root)
|
|
70
|
+
return rel.parts[0] # First dir is likely project name in a workspace
|
|
71
|
+
except ValueError:
|
|
72
|
+
return "local"
|
monoco/features/issue/models.py
CHANGED
|
@@ -78,16 +78,16 @@ class IsolationType(str, Enum):
|
|
|
78
78
|
WORKTREE = "worktree"
|
|
79
79
|
|
|
80
80
|
class IssueIsolation(BaseModel):
|
|
81
|
-
type:
|
|
81
|
+
type: str
|
|
82
82
|
ref: str # Git branch name
|
|
83
83
|
path: Optional[str] = None # Worktree path (relative to repo root or absolute)
|
|
84
84
|
created_at: datetime = Field(default_factory=current_time)
|
|
85
85
|
|
|
86
86
|
class IssueAction(BaseModel):
|
|
87
87
|
label: str
|
|
88
|
-
target_status: Optional[
|
|
89
|
-
target_stage: Optional[
|
|
90
|
-
target_solution: Optional[
|
|
88
|
+
target_status: Optional[str] = None
|
|
89
|
+
target_stage: Optional[str] = None
|
|
90
|
+
target_solution: Optional[str] = None
|
|
91
91
|
icon: Optional[str] = None
|
|
92
92
|
|
|
93
93
|
# Generic execution extensions
|
|
@@ -99,9 +99,9 @@ class IssueMetadata(BaseModel):
|
|
|
99
99
|
|
|
100
100
|
id: str
|
|
101
101
|
uid: Optional[str] = None # Global unique identifier for cross-project identity
|
|
102
|
-
type:
|
|
103
|
-
status:
|
|
104
|
-
stage: Optional[
|
|
102
|
+
type: str
|
|
103
|
+
status: str = "open"
|
|
104
|
+
stage: Optional[str] = None
|
|
105
105
|
title: str
|
|
106
106
|
|
|
107
107
|
# Time Anchors
|
|
@@ -112,7 +112,7 @@ class IssueMetadata(BaseModel):
|
|
|
112
112
|
|
|
113
113
|
parent: Optional[str] = None
|
|
114
114
|
sprint: Optional[str] = None
|
|
115
|
-
solution: Optional[
|
|
115
|
+
solution: Optional[str] = None
|
|
116
116
|
isolation: Optional[IssueIsolation] = None
|
|
117
117
|
dependencies: List[str] = []
|
|
118
118
|
related: List[str] = []
|
|
@@ -120,13 +120,30 @@ class IssueMetadata(BaseModel):
|
|
|
120
120
|
path: Optional[str] = None # Absolute path to the issue file
|
|
121
121
|
|
|
122
122
|
# Proxy UI Actions (Excluded from file persistence)
|
|
123
|
-
|
|
123
|
+
# Modified: Remove exclude=True to allow API/CLI inspection. Must be manually excluded during YAML Dump.
|
|
124
|
+
actions: List[IssueAction] = Field(default=[])
|
|
124
125
|
|
|
125
126
|
|
|
126
127
|
@model_validator(mode='before')
|
|
127
128
|
@classmethod
|
|
128
129
|
def normalize_fields(cls, v: Any) -> Any:
|
|
129
130
|
if isinstance(v, dict):
|
|
131
|
+
# Handle common capitalization variations for robustness
|
|
132
|
+
field_map = {
|
|
133
|
+
"ID": "id",
|
|
134
|
+
"Type": "type",
|
|
135
|
+
"Status": "status",
|
|
136
|
+
"Stage": "stage",
|
|
137
|
+
"Title": "title",
|
|
138
|
+
"Parent": "parent",
|
|
139
|
+
"Solution": "solution",
|
|
140
|
+
"Sprint": "sprint",
|
|
141
|
+
}
|
|
142
|
+
for old_k, new_k in field_map.items():
|
|
143
|
+
if old_k in v and new_k not in v:
|
|
144
|
+
v[new_k] = v[old_k] # Don't pop yet to avoid mutation issues if used elsewhere, or pop if safe.
|
|
145
|
+
# Pydantic v2 mode='before' is usually a copy if we want to be safe, but let's just add it.
|
|
146
|
+
|
|
130
147
|
# Normalize type and status to lowercase for compatibility
|
|
131
148
|
if "type" in v and isinstance(v["type"], str):
|
|
132
149
|
v["type"] = v["type"].lower()
|
|
@@ -38,7 +38,7 @@ Monoco 不仅仅复刻 Jira,而是基于 **"思维模式 (Mindset)"** 重新
|
|
|
38
38
|
- **Focus**: "How" (为了支撑系统运转,必须做什么)。
|
|
39
39
|
- **Prefix**: `CHORE-`
|
|
40
40
|
|
|
41
|
-
>
|
|
41
|
+
> 注: 取代了传统的 Task 概念。
|
|
42
42
|
|
|
43
43
|
#### 🐞 FIX (修复)
|
|
44
44
|
|
|
@@ -47,7 +47,7 @@ Monoco 不仅仅复刻 Jira,而是基于 **"思维模式 (Mindset)"** 重新
|
|
|
47
47
|
- **Focus**: "Fix" (恢复原状)。
|
|
48
48
|
- **Prefix**: `FIX-`
|
|
49
49
|
|
|
50
|
-
>
|
|
50
|
+
> 注: 取代了传统的 Bug 概念。
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -68,10 +68,9 @@ Monoco 不仅仅复刻 Jira,而是基于 **"思维模式 (Mindset)"** 重新
|
|
|
68
68
|
|
|
69
69
|
### 路径流转
|
|
70
70
|
|
|
71
|
-
使用 `monoco issue
|
|
71
|
+
使用 `monoco issue`:
|
|
72
72
|
|
|
73
73
|
1. **Create**: `monoco issue create <type> --title "..."`
|
|
74
|
-
|
|
75
74
|
- Params: `--parent <id>`, `--dependency <id>`, `--related <id>`, `--sprint <id>`, `--tags <tag>`
|
|
76
75
|
|
|
77
76
|
2. **Transition**: `monoco issue open/close/backlog <id>`
|
|
@@ -87,13 +86,13 @@ Monoco 不仅仅复刻 Jira,而是基于 **"思维模式 (Mindset)"** 重新
|
|
|
87
86
|
|
|
88
87
|
## 合规与结构校验 (Validation Rules)
|
|
89
88
|
|
|
90
|
-
为了确保数据严谨性,所有 Issue Ticket
|
|
89
|
+
为了确保数据严谨性,所有 Issue Ticket 必须遵循以下强制规则:
|
|
91
90
|
|
|
92
91
|
### 1. 结构一致性 (Structural Consistency)
|
|
93
92
|
|
|
94
93
|
- 必须包含一个二级标题 (`##`),内容必须与 Front Matter 中的 ID 和 Title 严格匹配。
|
|
95
|
-
-
|
|
96
|
-
-
|
|
94
|
+
- 格式: `## {ID}: {Title}`
|
|
95
|
+
- 示例: `## FEAT-0082: Issue Ticket Validator`
|
|
97
96
|
|
|
98
97
|
### 2. 内容完整性 (Content Completeness)
|
|
99
98
|
|
|
@@ -103,11 +102,11 @@ Monoco 不仅仅复刻 Jira,而是基于 **"思维模式 (Mindset)"** 重新
|
|
|
103
102
|
### 3. Checkbox 语法与层级 (Checkbox Matrix)
|
|
104
103
|
|
|
105
104
|
- 仅限使用: `- [ ]`, `- [x]`, `- [-]`, `- [/]`。
|
|
106
|
-
- **层级继承**: 若存在嵌套 Checkbox
|
|
105
|
+
- **层级继承**: 若存在嵌套 Checkbox,父项状态必须正确反映子项的聚合结果(例如: 任一子项为 `[/]` 则父项必为 `[/]`;子项全选则父项为 `[x]`)。
|
|
107
106
|
|
|
108
107
|
### 4. 状态矩阵 (State Matrix)
|
|
109
108
|
|
|
110
|
-
`status` (物理存放目录) 与 `stage` (Front Matter 字段)
|
|
109
|
+
`status` (物理存放目录) 与 `stage` (Front Matter 字段) 必须兼容:
|
|
111
110
|
|
|
112
111
|
- **open**: Draft, Doing, Review, Done
|
|
113
112
|
- **backlog**: Draft, Doing, Review
|