monoco-toolkit 0.3.3__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.
@@ -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
 
@@ -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 git
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
- def check_environment_policy(project_root: Path):
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(meta, content, all_issue_ids)
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
- # 0. Environment Policy Check (Guardrail)
176
- # We assume issues_root.parent is the project root or close enough for git context
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
- file_diagnostics = validator.validate(meta, content, all_issue_ids)
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:
@@ -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. **Heading**: Must have `## {ID}: {Title}` (matches metadata).
15
- 2. **Checkboxes**: Min 2 using `- [ ]`, `- [x]`, `- [-]`, `- [/]`.
16
- 3. **Review**: `## Review Comments` section required for Review/Done stages.
17
- 4. **Environment Policies**:
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. **标题**: 必须包含 `## {ID}: {Title}` 标题(与 Front Matter 一致)。
15
- 2. **内容**: 至少 2 Checkbox,使用 `- [ ]`, `- [x]`, `- [-]`, `- [/]`。
16
- 3. **评审**: `review`/`done` 阶段必须包含 `## Review Comments` 章节且内容不为空。
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"