monoco-toolkit 0.3.2__py3-none-any.whl → 0.3.5__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/config.py +35 -0
- monoco/core/integrations.py +0 -6
- monoco/core/sync.py +6 -19
- monoco/features/issue/commands.py +24 -16
- monoco/features/issue/core.py +90 -39
- monoco/features/issue/domain/models.py +1 -0
- monoco/features/issue/domain_commands.py +47 -0
- monoco/features/issue/domain_service.py +69 -0
- monoco/features/issue/linter.py +153 -50
- 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 +102 -0
- monoco/features/issue/test_resolver.py +83 -0
- monoco/features/issue/validator.py +97 -21
- monoco/features/scheduler/__init__.py +19 -0
- monoco/features/scheduler/cli.py +285 -0
- monoco/features/scheduler/config.py +68 -0
- monoco/features/scheduler/defaults.py +54 -0
- monoco/features/scheduler/engines.py +149 -0
- monoco/features/scheduler/manager.py +49 -0
- monoco/features/scheduler/models.py +24 -0
- monoco/features/scheduler/reliability.py +106 -0
- monoco/features/scheduler/session.py +87 -0
- monoco/features/scheduler/worker.py +133 -0
- monoco/main.py +5 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/METADATA +37 -46
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/RECORD +31 -21
- monoco/core/agent/__init__.py +0 -3
- monoco/core/agent/action.py +0 -168
- monoco/core/agent/adapters.py +0 -133
- monoco/core/agent/protocol.py +0 -32
- monoco/core/agent/state.py +0 -106
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/licenses/LICENSE +0 -0
monoco/features/issue/linter.py
CHANGED
|
@@ -4,44 +4,16 @@ 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
|
-
from monoco.core.lsp import Diagnostic, DiagnosticSeverity
|
|
10
|
+
from monoco.core.lsp import Diagnostic, DiagnosticSeverity, Range, Position
|
|
11
11
|
|
|
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]:
|
|
@@ -68,19 +40,31 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
68
40
|
files.extend(status_dir.rglob("*.md"))
|
|
69
41
|
|
|
70
42
|
for f in files:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
43
|
+
try:
|
|
44
|
+
meta = core.parse_issue(f, raise_error=True)
|
|
45
|
+
if meta:
|
|
46
|
+
local_id = meta.id
|
|
47
|
+
full_id = f"{project_name}::{local_id}"
|
|
48
|
+
|
|
49
|
+
all_issue_ids.add(local_id)
|
|
50
|
+
all_issue_ids.add(full_id)
|
|
51
|
+
|
|
52
|
+
project_issues.append((f, meta, project_name))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
# Report parsing failure as diagnostic
|
|
55
|
+
d = Diagnostic(
|
|
56
|
+
range=Range(
|
|
57
|
+
start=Position(line=0, character=0),
|
|
58
|
+
end=Position(line=0, character=0),
|
|
59
|
+
),
|
|
60
|
+
message=f"Schema Error: {str(e)}",
|
|
61
|
+
severity=DiagnosticSeverity.Error,
|
|
62
|
+
source="System",
|
|
63
|
+
)
|
|
64
|
+
d.data = {"path": f}
|
|
65
|
+
diagnostics.append(d)
|
|
80
66
|
return project_issues
|
|
81
67
|
|
|
82
|
-
from monoco.core.config import get_config
|
|
83
|
-
|
|
84
68
|
conf = get_config(str(issues_root.parent))
|
|
85
69
|
|
|
86
70
|
# Identify local project name
|
|
@@ -96,6 +80,17 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
96
80
|
).exists():
|
|
97
81
|
workspace_root = parent
|
|
98
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
|
+
|
|
99
94
|
# Collect from local issues_root
|
|
100
95
|
all_issues.extend(collect_project_issues(issues_root, local_project_name))
|
|
101
96
|
|
|
@@ -126,11 +121,17 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
126
121
|
pass
|
|
127
122
|
|
|
128
123
|
# 2. Validation Phase
|
|
129
|
-
for path, meta in all_issues:
|
|
124
|
+
for path, meta, project_name in all_issues:
|
|
130
125
|
content = path.read_text() # Re-read content for validation
|
|
131
126
|
|
|
132
127
|
# A. Run Core Validator
|
|
133
|
-
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
|
+
)
|
|
134
135
|
|
|
135
136
|
# Add context to diagnostics (Path)
|
|
136
137
|
for d in file_diagnostics:
|
|
@@ -158,9 +159,8 @@ def run_lint(
|
|
|
158
159
|
format: Output format (table, json)
|
|
159
160
|
file_paths: Optional list of paths to files to validate (LSP/Pre-commit mode)
|
|
160
161
|
"""
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
check_environment_policy(issues_root.parent)
|
|
162
|
+
# No environment policy check here.
|
|
163
|
+
# Toolkit should remain focused on Issue integrity.
|
|
164
164
|
|
|
165
165
|
diagnostics = []
|
|
166
166
|
|
|
@@ -193,7 +193,7 @@ def run_lint(
|
|
|
193
193
|
|
|
194
194
|
# Parse and validate file
|
|
195
195
|
try:
|
|
196
|
-
meta = core.parse_issue(file)
|
|
196
|
+
meta = core.parse_issue(file, raise_error=True)
|
|
197
197
|
if not meta:
|
|
198
198
|
console.print(
|
|
199
199
|
f"[yellow]Warning:[/yellow] Failed to parse issue metadata from {file_path}. Skipping."
|
|
@@ -201,7 +201,16 @@ def run_lint(
|
|
|
201
201
|
continue
|
|
202
202
|
|
|
203
203
|
content = file.read_text()
|
|
204
|
-
|
|
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
|
+
)
|
|
205
214
|
|
|
206
215
|
# Add context
|
|
207
216
|
for d in file_diagnostics:
|
|
@@ -315,6 +324,36 @@ def run_lint(
|
|
|
315
324
|
new_content = "\n".join(lines) + "\n"
|
|
316
325
|
has_changes = True
|
|
317
326
|
|
|
327
|
+
if (
|
|
328
|
+
"Hierarchy Violation" in d.message
|
|
329
|
+
and "Epics must have a parent" in d.message
|
|
330
|
+
):
|
|
331
|
+
try:
|
|
332
|
+
fm_match = re.search(
|
|
333
|
+
r"^---(.*?)---", new_content, re.DOTALL | re.MULTILINE
|
|
334
|
+
)
|
|
335
|
+
if fm_match:
|
|
336
|
+
import yaml
|
|
337
|
+
|
|
338
|
+
fm_text = fm_match.group(1)
|
|
339
|
+
data = yaml.safe_load(fm_text) or {}
|
|
340
|
+
|
|
341
|
+
# Default to EPIC-0000
|
|
342
|
+
data["parent"] = "EPIC-0000"
|
|
343
|
+
|
|
344
|
+
new_fm_text = yaml.dump(
|
|
345
|
+
data, sort_keys=False, allow_unicode=True
|
|
346
|
+
)
|
|
347
|
+
# Replace FM block
|
|
348
|
+
new_content = new_content.replace(
|
|
349
|
+
fm_match.group(1), "\n" + new_fm_text
|
|
350
|
+
)
|
|
351
|
+
has_changes = True
|
|
352
|
+
except Exception as ex:
|
|
353
|
+
console.print(
|
|
354
|
+
f"[red]Failed to fix parent hierarchy: {ex}[/red]"
|
|
355
|
+
)
|
|
356
|
+
|
|
318
357
|
if "Tag Check: Missing required context tags" in d.message:
|
|
319
358
|
# Extract missing tags from message
|
|
320
359
|
# Message format: "Tag Check: Missing required context tags: #TAG1, #TAG2"
|
|
@@ -426,6 +465,70 @@ def run_lint(
|
|
|
426
465
|
except Exception as e:
|
|
427
466
|
console.print(f"[red]Failed to fix domains for {path.name}: {e}[/red]")
|
|
428
467
|
|
|
468
|
+
# Domain Alias Fix
|
|
469
|
+
try:
|
|
470
|
+
alias_fixes = [
|
|
471
|
+
d for d in current_file_diags if "Domain Alias:" in d.message
|
|
472
|
+
]
|
|
473
|
+
if alias_fixes:
|
|
474
|
+
fm_match = re.search(
|
|
475
|
+
r"^---(.*?)---", new_content, re.DOTALL | re.MULTILINE
|
|
476
|
+
)
|
|
477
|
+
if fm_match:
|
|
478
|
+
import yaml
|
|
479
|
+
|
|
480
|
+
fm_text = fm_match.group(1)
|
|
481
|
+
data = yaml.safe_load(fm_text) or {}
|
|
482
|
+
|
|
483
|
+
domain_changed = False
|
|
484
|
+
if "domains" in data and isinstance(data["domains"], list):
|
|
485
|
+
domains = data["domains"]
|
|
486
|
+
for d in alias_fixes:
|
|
487
|
+
# Parse message: Domain Alias: 'alias' is an alias for 'canonical'.
|
|
488
|
+
m = re.search(
|
|
489
|
+
r"Domain Alias: '([^']+)' is an alias for '([^']+)'",
|
|
490
|
+
d.message,
|
|
491
|
+
)
|
|
492
|
+
if m:
|
|
493
|
+
old_d = m.group(1)
|
|
494
|
+
new_d = m.group(2)
|
|
495
|
+
|
|
496
|
+
if old_d in domains:
|
|
497
|
+
domains = [
|
|
498
|
+
new_d if x == old_d else x for x in domains
|
|
499
|
+
]
|
|
500
|
+
domain_changed = True
|
|
501
|
+
|
|
502
|
+
if domain_changed:
|
|
503
|
+
data["domains"] = domains
|
|
504
|
+
new_fm_text = yaml.dump(
|
|
505
|
+
data, sort_keys=False, allow_unicode=True
|
|
506
|
+
)
|
|
507
|
+
new_content = new_content.replace(
|
|
508
|
+
fm_match.group(1), "\n" + new_fm_text
|
|
509
|
+
)
|
|
510
|
+
has_changes = True
|
|
511
|
+
|
|
512
|
+
# Write immediately if not handled by previous block?
|
|
513
|
+
# We are in standard flow where has_changes flag handles write at end of loop?
|
|
514
|
+
# Wait, the previous block (Missing domains) logic wrote internally ONLY if has_changes.
|
|
515
|
+
# AND it reset has_changes=False at start of try?
|
|
516
|
+
# Actually the previous block structure was separate try-except blocks.
|
|
517
|
+
# But here I am inserting AFTER the Missing Domains try-except (which was lines 390-442).
|
|
518
|
+
# But I need to write if I changed it.
|
|
519
|
+
path.write_text(new_content)
|
|
520
|
+
if not any(path == p for p in processed_paths):
|
|
521
|
+
fixed_count += 1
|
|
522
|
+
processed_paths.add(path)
|
|
523
|
+
console.print(
|
|
524
|
+
f"[dim]Fixed (Domain Alias): {path.name}[/dim]"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
console.print(
|
|
529
|
+
f"[red]Failed to fix domain aliases for {path.name}: {e}[/red]"
|
|
530
|
+
)
|
|
531
|
+
|
|
429
532
|
console.print(f"[green]Applied auto-fixes to {fixed_count} files.[/green]")
|
|
430
533
|
|
|
431
534
|
# Re-run validation to verify
|
|
@@ -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,102 @@
|
|
|
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
|
+
)
|
|
20
|
+
|
|
21
|
+
# Context: toolkit project, monoco workspace
|
|
22
|
+
# Available IDs include namespaced versions
|
|
23
|
+
all_ids = {"monoco::EPIC-0001", "toolkit::FEAT-0002"}
|
|
24
|
+
|
|
25
|
+
# 1. Test namespaced reference in body
|
|
26
|
+
content = """---
|
|
27
|
+
id: FEAT-0001
|
|
28
|
+
title: Test Issue
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## FEAT-0001: Test Issue
|
|
32
|
+
|
|
33
|
+
This depends on monoco::EPIC-0001 and toolkit::FEAT-0002.
|
|
34
|
+
Broken one: other::FIX-9999.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
diagnostics = validator.validate(
|
|
38
|
+
meta, content, all_ids, current_project="toolkit", workspace_root="monoco"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Should have 1 warning for other::FIX-9999
|
|
42
|
+
warnings = [d for d in diagnostics if "Broken Reference" in d.message]
|
|
43
|
+
assert len(warnings) == 1
|
|
44
|
+
assert "other::FIX-9999" in warnings[0].message
|
|
45
|
+
|
|
46
|
+
# 2. Test proximity resolution in body
|
|
47
|
+
content_proximity = """---
|
|
48
|
+
id: FEAT-0001
|
|
49
|
+
title: Test Issue
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## FEAT-0001: Test Issue
|
|
53
|
+
|
|
54
|
+
Referencing EPIC-0001 (should resolve to monoco::EPIC-0001 via root fallback).
|
|
55
|
+
Referencing FEAT-0002 (should resolve to toolkit::FEAT-0002 via proximity).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
diagnostics = validator.validate(
|
|
59
|
+
meta,
|
|
60
|
+
content_proximity,
|
|
61
|
+
all_ids,
|
|
62
|
+
current_project="toolkit",
|
|
63
|
+
workspace_root="monoco",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
warnings = [d for d in diagnostics if "Broken Reference" in d.message]
|
|
67
|
+
assert len(warnings) == 0 # Both should be resolved
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_validator_parent_resolution():
|
|
71
|
+
validator = IssueValidator()
|
|
72
|
+
# Epic in toolkit
|
|
73
|
+
meta = IssueMetadata(
|
|
74
|
+
id="EPIC-0100",
|
|
75
|
+
uid="111",
|
|
76
|
+
type="epic",
|
|
77
|
+
status="open",
|
|
78
|
+
stage="draft",
|
|
79
|
+
title="Sub Epic",
|
|
80
|
+
parent="EPIC-0000", # Root Epic in workspace root
|
|
81
|
+
created_at=datetime.now(),
|
|
82
|
+
opened_at=datetime.now(),
|
|
83
|
+
updated_at=datetime.now(),
|
|
84
|
+
domains=["intelligence"],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
all_ids = {"monoco::EPIC-0000", "toolkit::EPIC-0100"}
|
|
88
|
+
|
|
89
|
+
content = """---
|
|
90
|
+
id: EPIC-0100
|
|
91
|
+
parent: EPIC-0000
|
|
92
|
+
---
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Context: current=toolkit, root=monoco
|
|
96
|
+
diagnostics = validator.validate(
|
|
97
|
+
meta, content, all_ids, current_project="toolkit", workspace_root="monoco"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Should be valid via root fallback
|
|
101
|
+
errors = [d for d in diagnostics if d.severity == 1] # DiagnosticSeverity.Error
|
|
102
|
+
assert len(errors) == 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from monoco.features.issue.resolver import ReferenceResolver, ResolutionContext
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_resolve_explicit_namespace():
|
|
5
|
+
context = ResolutionContext(
|
|
6
|
+
current_project="toolkit",
|
|
7
|
+
workspace_root="monoco",
|
|
8
|
+
available_ids={"toolkit::FEAT-0001", "monoco::FEAT-0001", "EPIC-0001"},
|
|
9
|
+
)
|
|
10
|
+
resolver = ReferenceResolver(context)
|
|
11
|
+
|
|
12
|
+
# Explicitly project reference
|
|
13
|
+
assert resolver.resolve("toolkit::FEAT-0001") == "toolkit::FEAT-0001"
|
|
14
|
+
assert resolver.resolve("monoco::FEAT-0001") == "monoco::FEAT-0001"
|
|
15
|
+
|
|
16
|
+
# Non-existent namespace
|
|
17
|
+
assert resolver.resolve("other::FEAT-0001") is None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_resolve_proximity_current_project():
|
|
21
|
+
context = ResolutionContext(
|
|
22
|
+
current_project="toolkit",
|
|
23
|
+
workspace_root="monoco",
|
|
24
|
+
available_ids={
|
|
25
|
+
"toolkit::FEAT-0001",
|
|
26
|
+
"monoco::FEAT-0001",
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
resolver = ReferenceResolver(context)
|
|
30
|
+
|
|
31
|
+
# Should prefer current project
|
|
32
|
+
assert resolver.resolve("FEAT-0001") == "toolkit::FEAT-0001"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_resolve_root_fallback():
|
|
36
|
+
context = ResolutionContext(
|
|
37
|
+
current_project="toolkit",
|
|
38
|
+
workspace_root="monoco",
|
|
39
|
+
available_ids={
|
|
40
|
+
"monoco::EPIC-0000",
|
|
41
|
+
"toolkit::FEAT-0001",
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
resolver = ReferenceResolver(context)
|
|
45
|
+
|
|
46
|
+
# Should fallback to root if not in current
|
|
47
|
+
assert resolver.resolve("EPIC-0000") == "monoco::EPIC-0000"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_resolve_local_ids():
|
|
51
|
+
context = ResolutionContext(
|
|
52
|
+
current_project="toolkit",
|
|
53
|
+
workspace_root="monoco",
|
|
54
|
+
available_ids={
|
|
55
|
+
"EPIC-9999",
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
resolver = ReferenceResolver(context)
|
|
59
|
+
|
|
60
|
+
# Should resolve plain local IDs
|
|
61
|
+
assert resolver.resolve("EPIC-9999") == "EPIC-9999"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_priority_order():
|
|
65
|
+
context = ResolutionContext(
|
|
66
|
+
current_project="toolkit",
|
|
67
|
+
workspace_root="monoco",
|
|
68
|
+
available_ids={"toolkit::FEAT-0001", "monoco::FEAT-0001", "FEAT-0001"},
|
|
69
|
+
)
|
|
70
|
+
resolver = ReferenceResolver(context)
|
|
71
|
+
|
|
72
|
+
# Order: toolkit::FEAT-0001 > monoco::FEAT-0001 > FEAT-0001
|
|
73
|
+
assert resolver.resolve("FEAT-0001") == "toolkit::FEAT-0001"
|
|
74
|
+
|
|
75
|
+
# If removed from toolkit context
|
|
76
|
+
context.available_ids.remove("toolkit::FEAT-0001")
|
|
77
|
+
resolver = ReferenceResolver(context)
|
|
78
|
+
assert resolver.resolve("FEAT-0001") == "monoco::FEAT-0001"
|
|
79
|
+
|
|
80
|
+
# If removed from root context
|
|
81
|
+
context.available_ids.remove("monoco::FEAT-0001")
|
|
82
|
+
resolver = ReferenceResolver(context)
|
|
83
|
+
assert resolver.resolve("FEAT-0001") == "FEAT-0001"
|