monoco-toolkit 0.3.3__py3-none-any.whl → 0.3.6__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/features/issue/commands.py +0 -15
- monoco/features/issue/core.py +4 -0
- monoco/features/issue/engine/machine.py +0 -29
- monoco/features/issue/linter.py +35 -40
- monoco/features/issue/models.py +43 -13
- monoco/features/issue/resolver.py +177 -0
- monoco/features/issue/resources/en/AGENTS.md +6 -4
- monoco/features/issue/resources/zh/AGENTS.md +6 -4
- monoco/features/issue/test_priority_integration.py +103 -0
- monoco/features/issue/test_resolver.py +83 -0
- monoco/features/issue/validator.py +50 -21
- monoco/features/memo/__init__.py +3 -0
- monoco/features/memo/cli.py +90 -0
- monoco/features/memo/core.py +87 -0
- monoco/features/scheduler/cli.py +94 -13
- monoco/features/scheduler/config.py +51 -15
- monoco/features/scheduler/engines.py +149 -0
- monoco/features/scheduler/reliability.py +11 -4
- monoco/features/scheduler/session.py +3 -4
- monoco/features/scheduler/worker.py +9 -5
- monoco/main.py +5 -0
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.6.dist-info}/METADATA +37 -46
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.6.dist-info}/RECORD +26 -19
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.6.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.6.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -267,10 +267,6 @@ def start(
|
|
|
267
267
|
@app.command("submit")
|
|
268
268
|
def submit(
|
|
269
269
|
issue_id: str = typer.Argument(..., help="Issue ID to submit"),
|
|
270
|
-
prune: bool = typer.Option(
|
|
271
|
-
False, "--prune", help="Delete branch/worktree after submit"
|
|
272
|
-
),
|
|
273
|
-
force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
|
|
274
270
|
root: Optional[str] = typer.Option(
|
|
275
271
|
None, "--root", help="Override issues root directory"
|
|
276
272
|
),
|
|
@@ -292,22 +288,11 @@ def submit(
|
|
|
292
288
|
except Exception as e:
|
|
293
289
|
report_status = f"failed: {e}"
|
|
294
290
|
|
|
295
|
-
pruned_resources = []
|
|
296
|
-
if prune:
|
|
297
|
-
try:
|
|
298
|
-
pruned_resources = core.prune_issue_resources(
|
|
299
|
-
issues_root, issue_id, force, project_root
|
|
300
|
-
)
|
|
301
|
-
except Exception as e:
|
|
302
|
-
OutputManager.error(f"Prune Error: {e}")
|
|
303
|
-
raise typer.Exit(code=1)
|
|
304
|
-
|
|
305
291
|
OutputManager.print(
|
|
306
292
|
{
|
|
307
293
|
"issue": issue,
|
|
308
294
|
"status": "submitted",
|
|
309
295
|
"report": report_status,
|
|
310
|
-
"pruned": pruned_resources,
|
|
311
296
|
}
|
|
312
297
|
)
|
|
313
298
|
|
monoco/features/issue/core.py
CHANGED
|
@@ -221,6 +221,10 @@ def create_issue_file(
|
|
|
221
221
|
if not find_issue_path(issues_root, rel_id):
|
|
222
222
|
raise ValueError(f"Related issue {rel_id} not found.")
|
|
223
223
|
|
|
224
|
+
# Auto-assign default parent for non-epic types if not provided
|
|
225
|
+
if issue_type != IssueType.EPIC and not parent:
|
|
226
|
+
parent = "EPIC-0000"
|
|
227
|
+
|
|
224
228
|
issue_id = find_next_id(issue_type, issues_root)
|
|
225
229
|
base_type_dir = get_issue_dir(issue_type, issues_root)
|
|
226
230
|
target_dir = base_type_dir / status
|
|
@@ -169,35 +169,6 @@ class StateMachine:
|
|
|
169
169
|
if meta.stage is None:
|
|
170
170
|
meta.stage = "draft"
|
|
171
171
|
|
|
172
|
-
def validate_transition(
|
|
173
|
-
self,
|
|
174
|
-
from_status: str,
|
|
175
|
-
from_stage: Optional[str],
|
|
176
|
-
to_status: str,
|
|
177
|
-
to_stage: Optional[str],
|
|
178
|
-
solution: Optional[str] = None,
|
|
179
|
-
) -> None:
|
|
180
|
-
"""
|
|
181
|
-
Validate if a transition is allowed. Raises ValueError if not.
|
|
182
|
-
"""
|
|
183
|
-
if from_status == to_status and from_stage == to_stage:
|
|
184
|
-
return # No change is always allowed (unless we want to enforce specific updates)
|
|
185
|
-
|
|
186
|
-
transition = self.find_transition(
|
|
187
|
-
from_status, from_stage, to_status, to_stage, solution
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
if not transition:
|
|
191
|
-
raise ValueError(
|
|
192
|
-
f"Lifecycle Policy: Transition from {from_status}({from_stage if from_stage else 'None'}) "
|
|
193
|
-
f"to {to_status}({to_stage if to_stage else 'None'}) is not defined."
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
if transition.required_solution and solution != transition.required_solution:
|
|
197
|
-
raise ValueError(
|
|
198
|
-
f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'."
|
|
199
|
-
)
|
|
200
|
-
|
|
201
172
|
def enforce_policy(self, meta: IssueMetadata) -> None:
|
|
202
173
|
"""
|
|
203
174
|
Apply consistency rules to IssueMetadata.
|
monoco/features/issue/linter.py
CHANGED
|
@@ -4,7 +4,7 @@ from rich.console import Console
|
|
|
4
4
|
from rich.table import Table
|
|
5
5
|
import typer
|
|
6
6
|
import re
|
|
7
|
-
from monoco.core import
|
|
7
|
+
from monoco.core.config import get_config
|
|
8
8
|
from . import core
|
|
9
9
|
from .validator import IssueValidator
|
|
10
10
|
from monoco.core.lsp import Diagnostic, DiagnosticSeverity, Range, Position
|
|
@@ -12,36 +12,8 @@ from monoco.core.lsp import Diagnostic, DiagnosticSeverity, Range, Position
|
|
|
12
12
|
console = Console()
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Guardrail: Prevent direct modifications on protected branches (main/master).
|
|
18
|
-
"""
|
|
19
|
-
# Only enforce if it is a git repo
|
|
20
|
-
try:
|
|
21
|
-
if not git.is_git_repo(project_root):
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
current_branch = git.get_current_branch(project_root)
|
|
25
|
-
# Standard protected branches
|
|
26
|
-
if current_branch in ["main", "master", "production"]:
|
|
27
|
-
# Check if dirty (uncommitted changes)
|
|
28
|
-
changed_files = git.get_git_status(project_root)
|
|
29
|
-
if changed_files:
|
|
30
|
-
console.print("\n[bold red]🛑 Environment Policy Violation[/bold red]")
|
|
31
|
-
console.print(
|
|
32
|
-
f"You are modifying code directly on protected branch: [bold cyan]{current_branch}[/bold cyan]"
|
|
33
|
-
)
|
|
34
|
-
console.print(f"Found {len(changed_files)} uncommitted changes.")
|
|
35
|
-
console.print(
|
|
36
|
-
"[yellow]Action Required:[/yellow] Please stash your changes and switch to a feature branch."
|
|
37
|
-
)
|
|
38
|
-
console.print(" > git stash")
|
|
39
|
-
console.print(" > monoco issue start <ID> --branch")
|
|
40
|
-
console.print(" > git stash pop")
|
|
41
|
-
raise typer.Exit(code=1)
|
|
42
|
-
except Exception:
|
|
43
|
-
# Fail safe: Do not block linting if git check fails unexpectedly
|
|
44
|
-
pass
|
|
15
|
+
# Removed check_environment_policy as per project philosophy:
|
|
16
|
+
# Toolkit should not interfere with Git operations.
|
|
45
17
|
|
|
46
18
|
|
|
47
19
|
def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnostic]:
|
|
@@ -77,7 +49,7 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
77
49
|
all_issue_ids.add(local_id)
|
|
78
50
|
all_issue_ids.add(full_id)
|
|
79
51
|
|
|
80
|
-
project_issues.append((f, meta))
|
|
52
|
+
project_issues.append((f, meta, project_name))
|
|
81
53
|
except Exception as e:
|
|
82
54
|
# Report parsing failure as diagnostic
|
|
83
55
|
d = Diagnostic(
|
|
@@ -93,8 +65,6 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
93
65
|
diagnostics.append(d)
|
|
94
66
|
return project_issues
|
|
95
67
|
|
|
96
|
-
from monoco.core.config import get_config
|
|
97
|
-
|
|
98
68
|
conf = get_config(str(issues_root.parent))
|
|
99
69
|
|
|
100
70
|
# Identify local project name
|
|
@@ -110,6 +80,17 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
110
80
|
).exists():
|
|
111
81
|
workspace_root = parent
|
|
112
82
|
|
|
83
|
+
# Identify local project name
|
|
84
|
+
local_project_name = "local"
|
|
85
|
+
if conf and conf.project and conf.project.name:
|
|
86
|
+
local_project_name = conf.project.name.lower()
|
|
87
|
+
|
|
88
|
+
workspace_root_name = local_project_name
|
|
89
|
+
if workspace_root != issues_root.parent:
|
|
90
|
+
root_conf = get_config(str(workspace_root))
|
|
91
|
+
if root_conf and root_conf.project and root_conf.project.name:
|
|
92
|
+
workspace_root_name = root_conf.project.name.lower()
|
|
93
|
+
|
|
113
94
|
# Collect from local issues_root
|
|
114
95
|
all_issues.extend(collect_project_issues(issues_root, local_project_name))
|
|
115
96
|
|
|
@@ -140,11 +121,17 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
140
121
|
pass
|
|
141
122
|
|
|
142
123
|
# 2. Validation Phase
|
|
143
|
-
for path, meta in all_issues:
|
|
124
|
+
for path, meta, project_name in all_issues:
|
|
144
125
|
content = path.read_text() # Re-read content for validation
|
|
145
126
|
|
|
146
127
|
# A. Run Core Validator
|
|
147
|
-
file_diagnostics = validator.validate(
|
|
128
|
+
file_diagnostics = validator.validate(
|
|
129
|
+
meta,
|
|
130
|
+
content,
|
|
131
|
+
all_issue_ids,
|
|
132
|
+
current_project=project_name,
|
|
133
|
+
workspace_root=workspace_root_name,
|
|
134
|
+
)
|
|
148
135
|
|
|
149
136
|
# Add context to diagnostics (Path)
|
|
150
137
|
for d in file_diagnostics:
|
|
@@ -172,9 +159,8 @@ def run_lint(
|
|
|
172
159
|
format: Output format (table, json)
|
|
173
160
|
file_paths: Optional list of paths to files to validate (LSP/Pre-commit mode)
|
|
174
161
|
"""
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
check_environment_policy(issues_root.parent)
|
|
162
|
+
# No environment policy check here.
|
|
163
|
+
# Toolkit should remain focused on Issue integrity.
|
|
178
164
|
|
|
179
165
|
diagnostics = []
|
|
180
166
|
|
|
@@ -215,7 +201,16 @@ def run_lint(
|
|
|
215
201
|
continue
|
|
216
202
|
|
|
217
203
|
content = file.read_text()
|
|
218
|
-
|
|
204
|
+
|
|
205
|
+
# Try to resolve current project name for context
|
|
206
|
+
current_project_name = "local"
|
|
207
|
+
conf = get_config(str(issues_root.parent))
|
|
208
|
+
if conf and conf.project and conf.project.name:
|
|
209
|
+
current_project_name = conf.project.name.lower()
|
|
210
|
+
|
|
211
|
+
file_diagnostics = validator.validate(
|
|
212
|
+
meta, content, all_issue_ids, current_project=current_project_name
|
|
213
|
+
)
|
|
219
214
|
|
|
220
215
|
# Add context
|
|
221
216
|
for d in file_diagnostics:
|
monoco/features/issue/models.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import List, Optional, Any, Dict
|
|
3
|
-
from pydantic import BaseModel, Field, model_validator
|
|
3
|
+
from pydantic import BaseModel, Field, model_validator, ConfigDict
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
import hashlib
|
|
6
6
|
import secrets
|
|
@@ -105,13 +105,13 @@ class IssueAction(BaseModel):
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
class IssueMetadata(BaseModel):
|
|
108
|
-
model_config =
|
|
108
|
+
model_config = ConfigDict(extra="allow", validate_assignment=True)
|
|
109
109
|
|
|
110
110
|
id: str
|
|
111
111
|
uid: Optional[str] = None # Global unique identifier for cross-project identity
|
|
112
|
-
type:
|
|
113
|
-
status:
|
|
114
|
-
stage: Optional[
|
|
112
|
+
type: IssueType
|
|
113
|
+
status: IssueStatus = IssueStatus.OPEN
|
|
114
|
+
stage: Optional[IssueStage] = None
|
|
115
115
|
title: str
|
|
116
116
|
|
|
117
117
|
# Time Anchors
|
|
@@ -122,7 +122,7 @@ class IssueMetadata(BaseModel):
|
|
|
122
122
|
|
|
123
123
|
parent: Optional[str] = None
|
|
124
124
|
sprint: Optional[str] = None
|
|
125
|
-
solution: Optional[
|
|
125
|
+
solution: Optional[IssueSolution] = None
|
|
126
126
|
isolation: Optional[IssueIsolation] = None
|
|
127
127
|
dependencies: List[str] = []
|
|
128
128
|
related: List[str] = []
|
|
@@ -161,26 +161,56 @@ class IssueMetadata(BaseModel):
|
|
|
161
161
|
# Normalize type and status to lowercase for compatibility
|
|
162
162
|
if "type" in v and isinstance(v["type"], str):
|
|
163
163
|
v["type"] = v["type"].lower()
|
|
164
|
+
try:
|
|
165
|
+
v["type"] = IssueType(v["type"])
|
|
166
|
+
except ValueError:
|
|
167
|
+
pass
|
|
168
|
+
|
|
164
169
|
if "status" in v and isinstance(v["status"], str):
|
|
165
170
|
v["status"] = v["status"].lower()
|
|
171
|
+
try:
|
|
172
|
+
v["status"] = IssueStatus(v["status"])
|
|
173
|
+
except ValueError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
166
176
|
if "solution" in v and isinstance(v["solution"], str):
|
|
167
177
|
v["solution"] = v["solution"].lower()
|
|
178
|
+
try:
|
|
179
|
+
v["solution"] = IssueSolution(v["solution"])
|
|
180
|
+
except ValueError:
|
|
181
|
+
pass
|
|
182
|
+
|
|
168
183
|
# Stage normalization
|
|
169
184
|
if "stage" in v and isinstance(v["stage"], str):
|
|
170
185
|
v["stage"] = v["stage"].lower()
|
|
171
186
|
if v["stage"] == "todo":
|
|
172
187
|
v["stage"] = "draft"
|
|
188
|
+
try:
|
|
189
|
+
v["stage"] = IssueStage(v["stage"])
|
|
190
|
+
except ValueError:
|
|
191
|
+
pass
|
|
173
192
|
return v
|
|
174
193
|
|
|
175
194
|
@model_validator(mode="after")
|
|
176
195
|
def validate_lifecycle(self) -> "IssueMetadata":
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
# 1. Solution Consistency: Closed issues MUST have a solution
|
|
197
|
+
if self.status == IssueStatus.CLOSED and not self.solution:
|
|
198
|
+
raise ValueError(f"Issue '{self.id}' is closed but 'solution' is missing.")
|
|
199
|
+
|
|
200
|
+
# 2. Hierarchy Consistency: non-epic types MUST have a parent (except specific root seeds)
|
|
201
|
+
if self.type != IssueType.EPIC and not self.parent:
|
|
202
|
+
# We allow exceptions for very specific bootstrap cases if needed, but currently enforce it.
|
|
203
|
+
if self.id not in ["FEAT-BOOTSTRAP"]: # Example exception
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"Issue '{self.id}' of type '{self.type}' must have a 'parent' reference."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# 3. State/Stage Consistency (Warnings or Errors)
|
|
209
|
+
# Note: In Monoco, status: closed is tightly coupled with stage: done
|
|
210
|
+
if self.status == IssueStatus.CLOSED and self.stage != IssueStage.DONE:
|
|
211
|
+
# We could auto-fix here, but let's be strict for Validation purposes
|
|
212
|
+
# raise ValueError(f"Issue '{self.id}' is closed but stage is '{self.stage}' (expected 'done').")
|
|
213
|
+
pass
|
|
184
214
|
|
|
185
215
|
return self
|
|
186
216
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reference Resolution Engine for Multi-Project Environments.
|
|
3
|
+
|
|
4
|
+
This module implements a priority-based resolution strategy for Issue ID references
|
|
5
|
+
in multi-project/workspace environments.
|
|
6
|
+
|
|
7
|
+
Resolution Priority:
|
|
8
|
+
1. Explicit Namespace (namespace::ID) - Highest priority
|
|
9
|
+
2. Proximity Rule (Current Project Context)
|
|
10
|
+
3. Root Fallback (Workspace Root)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional, Set, Dict
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ResolutionContext:
|
|
19
|
+
"""Context information for reference resolution."""
|
|
20
|
+
|
|
21
|
+
current_project: str
|
|
22
|
+
"""Name of the current project (e.g., 'toolkit', 'typedown')."""
|
|
23
|
+
|
|
24
|
+
workspace_root: Optional[str] = None
|
|
25
|
+
"""Name of the workspace root project (e.g., 'monoco')."""
|
|
26
|
+
|
|
27
|
+
available_ids: Set[str] = None
|
|
28
|
+
"""Set of all available Issue IDs (both local and namespaced)."""
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
if self.available_ids is None:
|
|
32
|
+
self.available_ids = set()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReferenceResolver:
|
|
36
|
+
"""
|
|
37
|
+
Resolves Issue ID references with multi-project awareness.
|
|
38
|
+
|
|
39
|
+
Supports:
|
|
40
|
+
- Explicit namespace syntax: `namespace::ID`
|
|
41
|
+
- Proximity-based resolution
|
|
42
|
+
- Root fallback for global issues
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, context: ResolutionContext):
|
|
46
|
+
self.context = context
|
|
47
|
+
|
|
48
|
+
# Build index for fast lookup
|
|
49
|
+
self._local_ids: Set[str] = set()
|
|
50
|
+
self._namespaced_ids: Dict[str, Set[str]] = {}
|
|
51
|
+
|
|
52
|
+
for issue_id in context.available_ids:
|
|
53
|
+
if "::" in issue_id:
|
|
54
|
+
# Namespaced ID
|
|
55
|
+
namespace, local_id = issue_id.split("::", 1)
|
|
56
|
+
if namespace not in self._namespaced_ids:
|
|
57
|
+
self._namespaced_ids[namespace] = set()
|
|
58
|
+
self._namespaced_ids[namespace].add(local_id)
|
|
59
|
+
else:
|
|
60
|
+
# Local ID
|
|
61
|
+
self._local_ids.add(issue_id)
|
|
62
|
+
|
|
63
|
+
def resolve(self, reference: str) -> Optional[str]:
|
|
64
|
+
"""
|
|
65
|
+
Resolve an Issue ID reference to its canonical form.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
reference: The reference to resolve (e.g., "FEAT-0001" or "toolkit::FEAT-0001")
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The canonical ID if found, None otherwise.
|
|
72
|
+
For namespaced IDs, returns the full form (e.g., "toolkit::FEAT-0001").
|
|
73
|
+
For local IDs, returns the short form (e.g., "FEAT-0001").
|
|
74
|
+
|
|
75
|
+
Resolution Strategy:
|
|
76
|
+
1. If reference contains "::", treat as explicit namespace
|
|
77
|
+
2. Otherwise, apply proximity rule:
|
|
78
|
+
a. Check current project context
|
|
79
|
+
b. Check workspace root (if different from current)
|
|
80
|
+
c. Check if exists as local ID
|
|
81
|
+
"""
|
|
82
|
+
# Strategy 1: Explicit Namespace
|
|
83
|
+
if "::" in reference:
|
|
84
|
+
return self._resolve_explicit(reference)
|
|
85
|
+
|
|
86
|
+
# Strategy 2: Proximity Rule
|
|
87
|
+
return self._resolve_proximity(reference)
|
|
88
|
+
|
|
89
|
+
def _resolve_explicit(self, reference: str) -> Optional[str]:
|
|
90
|
+
"""Resolve explicitly namespaced reference."""
|
|
91
|
+
if reference in self.context.available_ids:
|
|
92
|
+
return reference
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def _resolve_proximity(self, reference: str) -> Optional[str]:
|
|
96
|
+
"""
|
|
97
|
+
Resolve reference using proximity rule.
|
|
98
|
+
|
|
99
|
+
Priority:
|
|
100
|
+
1. Current project namespace
|
|
101
|
+
2. Workspace root namespace
|
|
102
|
+
3. Local (unnamespaced) ID
|
|
103
|
+
"""
|
|
104
|
+
# Priority 1: Current project
|
|
105
|
+
current_namespaced = f"{self.context.current_project}::{reference}"
|
|
106
|
+
if current_namespaced in self.context.available_ids:
|
|
107
|
+
return current_namespaced
|
|
108
|
+
|
|
109
|
+
# Priority 2: Workspace root (if different from current)
|
|
110
|
+
if (
|
|
111
|
+
self.context.workspace_root
|
|
112
|
+
and self.context.workspace_root != self.context.current_project
|
|
113
|
+
):
|
|
114
|
+
root_namespaced = f"{self.context.workspace_root}::{reference}"
|
|
115
|
+
if root_namespaced in self.context.available_ids:
|
|
116
|
+
return root_namespaced
|
|
117
|
+
|
|
118
|
+
# Priority 3: Local ID
|
|
119
|
+
if reference in self._local_ids:
|
|
120
|
+
return reference
|
|
121
|
+
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def is_valid_reference(self, reference: str) -> bool:
|
|
125
|
+
"""Check if a reference can be resolved."""
|
|
126
|
+
return self.resolve(reference) is not None
|
|
127
|
+
|
|
128
|
+
def get_resolution_chain(self, reference: str) -> list[str]:
|
|
129
|
+
"""
|
|
130
|
+
Get the resolution chain for debugging purposes.
|
|
131
|
+
|
|
132
|
+
Returns a list of candidate IDs that were checked in order.
|
|
133
|
+
"""
|
|
134
|
+
chain = []
|
|
135
|
+
|
|
136
|
+
if "::" in reference:
|
|
137
|
+
chain.append(reference)
|
|
138
|
+
else:
|
|
139
|
+
# Proximity chain
|
|
140
|
+
chain.append(f"{self.context.current_project}::{reference}")
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
self.context.workspace_root
|
|
144
|
+
and self.context.workspace_root != self.context.current_project
|
|
145
|
+
):
|
|
146
|
+
chain.append(f"{self.context.workspace_root}::{reference}")
|
|
147
|
+
|
|
148
|
+
chain.append(reference)
|
|
149
|
+
|
|
150
|
+
return chain
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def resolve_reference(
|
|
154
|
+
reference: str,
|
|
155
|
+
context_project: str,
|
|
156
|
+
available_ids: Set[str],
|
|
157
|
+
workspace_root: Optional[str] = None,
|
|
158
|
+
) -> Optional[str]:
|
|
159
|
+
"""
|
|
160
|
+
Convenience function for resolving a single reference.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
reference: The Issue ID to resolve
|
|
164
|
+
context_project: Current project name
|
|
165
|
+
available_ids: Set of all available Issue IDs
|
|
166
|
+
workspace_root: Optional workspace root project name
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Resolved canonical ID or None if not found
|
|
170
|
+
"""
|
|
171
|
+
context = ResolutionContext(
|
|
172
|
+
current_project=context_project,
|
|
173
|
+
workspace_root=workspace_root,
|
|
174
|
+
available_ids=available_ids,
|
|
175
|
+
)
|
|
176
|
+
resolver = ReferenceResolver(context)
|
|
177
|
+
return resolver.resolve(reference)
|
|
@@ -11,10 +11,12 @@ System for managing tasks using `monoco issue`.
|
|
|
11
11
|
- **Sync Context**: `monoco issue sync-files [id]` (Update file tracking)
|
|
12
12
|
- **Structure**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (e.g. `Issues/Features/open/`). Do not deviate.
|
|
13
13
|
- **Rules**:
|
|
14
|
-
1. **
|
|
15
|
-
2. **
|
|
16
|
-
3. **
|
|
17
|
-
4. **
|
|
14
|
+
1. **Issue First**: You MUST create an Issue (`monoco issue create`) before starting any work (research, design, or drafting).
|
|
15
|
+
2. **Heading**: Must have `## {ID}: {Title}` (matches metadata).
|
|
16
|
+
3. **Checkboxes**: Min 2 using `- [ ]`, `- [x]`, `- [-]`, `- [/]`.
|
|
17
|
+
4. **Review**: `## Review Comments` section required for Review/Done stages.
|
|
18
|
+
5. **Environment Policies**:
|
|
18
19
|
- Must use `monoco issue start --branch`.
|
|
19
20
|
- 🛑 **NO** direct coding on `main`/`master` (Linter will fail).
|
|
21
|
+
- **Prune Timing**: ONLY prune environment (branch/worktree) during `monoco issue close --prune`. NEVER prune at `submit` stage.
|
|
20
22
|
- Must update `files` field after coding (via `sync-files` or manual).
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
- **上下文同步**: `monoco issue sync-files [id]` (更新文件追踪)
|
|
12
12
|
- **结构**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (如 `Issues/Features/open/`)。
|
|
13
13
|
- **强制规则**:
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
14
|
+
1. **先有 Issue**: 在进行任何调研、设计或 Draft 之前,必须先使用 `monoco issue create` 创建 Issue。
|
|
15
|
+
2. **标题**: 必须包含 `## {ID}: {Title}` 标题(与 Front Matter 一致)。
|
|
16
|
+
3. **内容**: 至少 2 个 Checkbox,使用 `- [ ]`, `- [x]`, `- [-]`, `- [/]`。
|
|
17
|
+
4. **评审**: `review`/`done` 阶段必须包含 `## Review Comments` 章节且内容不为空。
|
|
18
|
+
5. **环境策略**:
|
|
18
19
|
- 必须使用 `monoco issue start --branch` 创建 Feature 分支。
|
|
19
20
|
- 🛑 **禁止**直接在 `main`/`master` 分支修改代码 (Linter 会报错)。
|
|
21
|
+
- **清理时机**: 环境清理仅应在 `close` 时执行。**禁止**在 `submit` 阶段清理环境。
|
|
20
22
|
- 修改代码后**必须**更新 `files` 字段(通过 `sync-files` 或手动)。
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from monoco.features.issue.validator import IssueValidator
|
|
2
|
+
from monoco.features.issue.models import IssueMetadata
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_validator_namespaced_reference_in_body():
|
|
7
|
+
validator = IssueValidator()
|
|
8
|
+
meta = IssueMetadata(
|
|
9
|
+
id="FEAT-0001",
|
|
10
|
+
uid="123456",
|
|
11
|
+
type="feature",
|
|
12
|
+
status="open",
|
|
13
|
+
stage="draft",
|
|
14
|
+
title="Test Issue",
|
|
15
|
+
created_at=datetime.now(),
|
|
16
|
+
opened_at=datetime.now(),
|
|
17
|
+
updated_at=datetime.now(),
|
|
18
|
+
domains=["intelligence"],
|
|
19
|
+
parent="EPIC-0000",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Context: toolkit project, monoco workspace
|
|
23
|
+
# Available IDs include namespaced versions
|
|
24
|
+
all_ids = {"monoco::EPIC-0001", "toolkit::FEAT-0002"}
|
|
25
|
+
|
|
26
|
+
# 1. Test namespaced reference in body
|
|
27
|
+
content = """---
|
|
28
|
+
id: FEAT-0001
|
|
29
|
+
title: Test Issue
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## FEAT-0001: Test Issue
|
|
33
|
+
|
|
34
|
+
This depends on monoco::EPIC-0001 and toolkit::FEAT-0002.
|
|
35
|
+
Broken one: other::FIX-9999.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
diagnostics = validator.validate(
|
|
39
|
+
meta, content, all_ids, current_project="toolkit", workspace_root="monoco"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Should have 1 warning for other::FIX-9999
|
|
43
|
+
warnings = [d for d in diagnostics if "Broken Reference" in d.message]
|
|
44
|
+
assert len(warnings) == 1
|
|
45
|
+
assert "other::FIX-9999" in warnings[0].message
|
|
46
|
+
|
|
47
|
+
# 2. Test proximity resolution in body
|
|
48
|
+
content_proximity = """---
|
|
49
|
+
id: FEAT-0001
|
|
50
|
+
title: Test Issue
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## FEAT-0001: Test Issue
|
|
54
|
+
|
|
55
|
+
Referencing EPIC-0001 (should resolve to monoco::EPIC-0001 via root fallback).
|
|
56
|
+
Referencing FEAT-0002 (should resolve to toolkit::FEAT-0002 via proximity).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
diagnostics = validator.validate(
|
|
60
|
+
meta,
|
|
61
|
+
content_proximity,
|
|
62
|
+
all_ids,
|
|
63
|
+
current_project="toolkit",
|
|
64
|
+
workspace_root="monoco",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
warnings = [d for d in diagnostics if "Broken Reference" in d.message]
|
|
68
|
+
assert len(warnings) == 0 # Both should be resolved
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_validator_parent_resolution():
|
|
72
|
+
validator = IssueValidator()
|
|
73
|
+
# Epic in toolkit
|
|
74
|
+
meta = IssueMetadata(
|
|
75
|
+
id="EPIC-0100",
|
|
76
|
+
uid="111",
|
|
77
|
+
type="epic",
|
|
78
|
+
status="open",
|
|
79
|
+
stage="draft",
|
|
80
|
+
title="Sub Epic",
|
|
81
|
+
parent="EPIC-0000", # Root Epic in workspace root
|
|
82
|
+
created_at=datetime.now(),
|
|
83
|
+
opened_at=datetime.now(),
|
|
84
|
+
updated_at=datetime.now(),
|
|
85
|
+
domains=["intelligence"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
all_ids = {"monoco::EPIC-0000", "toolkit::EPIC-0100"}
|
|
89
|
+
|
|
90
|
+
content = """---
|
|
91
|
+
id: EPIC-0100
|
|
92
|
+
parent: EPIC-0000
|
|
93
|
+
---
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Context: current=toolkit, root=monoco
|
|
97
|
+
diagnostics = validator.validate(
|
|
98
|
+
meta, content, all_ids, current_project="toolkit", workspace_root="monoco"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Should be valid via root fallback
|
|
102
|
+
errors = [d for d in diagnostics if d.severity == 1] # DiagnosticSeverity.Error
|
|
103
|
+
assert len(errors) == 0
|