monoco-toolkit 0.3.6__py3-none-any.whl → 0.3.10__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/cli/workspace.py +1 -1
- monoco/core/config.py +58 -0
- monoco/core/hooks/__init__.py +19 -0
- monoco/core/hooks/base.py +104 -0
- monoco/core/hooks/builtin/__init__.py +11 -0
- monoco/core/hooks/builtin/git_cleanup.py +266 -0
- monoco/core/hooks/builtin/logging_hook.py +78 -0
- monoco/core/hooks/context.py +131 -0
- monoco/core/hooks/registry.py +222 -0
- monoco/core/injection.py +63 -29
- monoco/core/integrations.py +8 -2
- monoco/core/output.py +5 -5
- monoco/core/registry.py +9 -1
- monoco/core/resource/__init__.py +5 -0
- monoco/core/resource/finder.py +98 -0
- monoco/core/resource/manager.py +91 -0
- monoco/core/resource/models.py +35 -0
- monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/setup.py +1 -1
- monoco/core/skill_framework.py +292 -0
- monoco/core/skills.py +538 -254
- monoco/core/sync.py +73 -1
- monoco/core/workflow_converter.py +420 -0
- monoco/features/{scheduler → agent}/__init__.py +5 -3
- monoco/features/agent/adapter.py +31 -0
- monoco/features/agent/apoptosis.py +44 -0
- monoco/features/agent/cli.py +296 -0
- monoco/features/agent/config.py +96 -0
- monoco/features/agent/defaults.py +12 -0
- monoco/features/{scheduler → agent}/engines.py +32 -6
- monoco/features/agent/flow_skills.py +281 -0
- monoco/features/agent/manager.py +91 -0
- monoco/features/{scheduler → agent}/models.py +6 -3
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
- monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
- monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
- monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
- monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
- monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
- monoco/features/agent/resources/roles/role-manager.yaml +46 -0
- monoco/features/agent/resources/roles/role-planner.yaml +46 -0
- monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
- monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
- monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
- monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
- monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
- monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
- monoco/features/{scheduler → agent}/session.py +36 -1
- monoco/features/{scheduler → agent}/worker.py +40 -4
- monoco/features/glossary/adapter.py +31 -0
- monoco/features/glossary/config.py +5 -0
- monoco/features/glossary/resources/en/AGENTS.md +29 -0
- monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/glossary/resources/zh/AGENTS.md +29 -0
- monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/issue/commands.py +427 -21
- monoco/features/issue/core.py +140 -1
- monoco/features/issue/criticality.py +553 -0
- monoco/features/issue/domain/models.py +28 -2
- monoco/features/issue/engine/machine.py +75 -15
- monoco/features/issue/git_service.py +185 -0
- monoco/features/issue/linter.py +291 -62
- monoco/features/issue/models.py +50 -2
- monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
- monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -0
- monoco/features/issue/validator.py +185 -65
- monoco/features/memo/__init__.py +2 -1
- monoco/features/memo/adapter.py +32 -0
- monoco/features/memo/cli.py +36 -14
- monoco/features/memo/core.py +59 -0
- monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
- monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/memo/resources/zh/AGENTS.md +8 -0
- monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
- monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
- monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
- monoco/main.py +2 -3
- monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
- monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
- monoco/features/scheduler/cli.py +0 -285
- monoco/features/scheduler/config.py +0 -68
- monoco/features/scheduler/defaults.py +0 -54
- monoco/features/scheduler/manager.py +0 -49
- monoco/features/scheduler/reliability.py +0 -106
- monoco/features/skills/core.py +0 -102
- monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
- monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
- /monoco/core/{hooks.py → githooks.py} +0 -0
- /monoco/features/{skills → glossary}/__init__.py +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/licenses/LICENSE +0 -0
monoco/features/issue/linter.py
CHANGED
|
@@ -24,46 +24,201 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
24
24
|
validator = IssueValidator(issues_root)
|
|
25
25
|
|
|
26
26
|
all_issue_ids = set()
|
|
27
|
+
id_to_path = {}
|
|
27
28
|
all_issues = []
|
|
28
29
|
|
|
29
30
|
# 1. Collection Phase (Build Index)
|
|
30
31
|
# Helper to collect issues from a project
|
|
31
32
|
def collect_project_issues(project_issues_root: Path, project_name: str = "local"):
|
|
32
33
|
project_issues = []
|
|
33
|
-
|
|
34
|
+
project_diagnostics = []
|
|
35
|
+
for subdir in ["Epics", "Features", "Chores", "Fixes", "Domains"]:
|
|
34
36
|
d = project_issues_root / subdir
|
|
35
37
|
if d.exists():
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
38
|
+
if subdir == "Domains":
|
|
39
|
+
# Special handling for Domains (not Issue tickets)
|
|
40
|
+
for f in d.rglob("*.md"):
|
|
41
|
+
# Domain validation happens here inline or via separate validator
|
|
42
|
+
# For now, we just index them for reference validation
|
|
43
|
+
domain_key = f.stem
|
|
44
|
+
# Ensure H1 matches filename
|
|
45
|
+
try:
|
|
46
|
+
content = f.read_text(encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
# 1. H1 Check
|
|
49
|
+
h1_match = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
|
|
50
|
+
if not h1_match:
|
|
51
|
+
project_diagnostics.append(
|
|
52
|
+
Diagnostic(
|
|
53
|
+
range=Range(
|
|
54
|
+
start=Position(line=0, character=0),
|
|
55
|
+
end=Position(line=0, character=0),
|
|
56
|
+
),
|
|
57
|
+
message=f"Domain '{f.name}' missing H1 title.",
|
|
58
|
+
severity=DiagnosticSeverity.Error,
|
|
59
|
+
source="DomainValidator",
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
h1_title = h1_match.group(1).strip()
|
|
64
|
+
# Allow exact match or "Domain: Name" pattern? User said "Title should be same as filename"
|
|
65
|
+
# But let's be strict: Filename stem MUST match H1
|
|
66
|
+
# User spec: "标题应该和文件名相同" -> The H1 content (after #) must equal filename stem (spaces sensitive).
|
|
67
|
+
# But wait, user example "Domain: Agent Onboarding" (bad) vs "Agent Onboarding" (good?).
|
|
68
|
+
# Actually user said "Attribute key in yaml" should match.
|
|
69
|
+
# Let's enforce: Filename 'Agent Onboarding.md' -> H1 '# Agent Onboarding'
|
|
70
|
+
|
|
71
|
+
# Check for "Domain: " prefix which is forbidden
|
|
72
|
+
if h1_title.lower().startswith("domain:"):
|
|
73
|
+
project_diagnostics.append(
|
|
74
|
+
Diagnostic(
|
|
75
|
+
range=Range(
|
|
76
|
+
start=Position(line=0, character=0),
|
|
77
|
+
end=Position(line=0, character=0),
|
|
78
|
+
),
|
|
79
|
+
message=f"Domain H1 must not use 'Domain:' prefix. Found '{h1_title}'.",
|
|
80
|
+
severity=DiagnosticSeverity.Error,
|
|
81
|
+
source="DomainValidator",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
elif h1_title != f.stem:
|
|
85
|
+
project_diagnostics.append(
|
|
86
|
+
Diagnostic(
|
|
87
|
+
range=Range(
|
|
88
|
+
start=Position(line=0, character=0),
|
|
89
|
+
end=Position(line=0, character=0),
|
|
90
|
+
),
|
|
91
|
+
message=f"Domain H1 '{h1_title}' does not match filename '{f.stem}'.",
|
|
92
|
+
severity=DiagnosticSeverity.Error,
|
|
93
|
+
source="DomainValidator",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# 2. Source Language Check
|
|
98
|
+
# We import the check from i18n core if not available in validator yet
|
|
99
|
+
# But linter.py imports core... issue core.
|
|
100
|
+
|
|
101
|
+
# Need source lang. Conf is available below, let's grab it or pass it.
|
|
102
|
+
# We are inside a helper func, need to access outer scope or pass config.
|
|
103
|
+
# We'll do it in validation phase? Or here?
|
|
104
|
+
# 'conf' is defined in outer scope but not passed to this helper.
|
|
105
|
+
# Let's resolve config inside loop or pass it.
|
|
106
|
+
# To keep it simple, we do light check here.
|
|
107
|
+
|
|
108
|
+
# Actually, we should collect Domains into a list to pass to Validator
|
|
109
|
+
# so Validator can check if Issue 'domains' field references valid domains.
|
|
110
|
+
# We'll use a special set for this.
|
|
111
|
+
project_issues.append(
|
|
112
|
+
(f, "DOMAIN", f.stem)
|
|
113
|
+
) # Marker for later
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
project_diagnostics.append(
|
|
117
|
+
Diagnostic(
|
|
118
|
+
range=Range(
|
|
119
|
+
start=Position(line=0, character=0),
|
|
120
|
+
end=Position(line=0, character=0),
|
|
121
|
+
),
|
|
122
|
+
message=f"Domain Read Error: {e}",
|
|
123
|
+
severity=DiagnosticSeverity.Error,
|
|
124
|
+
source="DomainValidator",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
# Standard Issues (Epics/Features/etc)
|
|
130
|
+
files = []
|
|
131
|
+
for status in ["open", "closed", "backlog"]:
|
|
132
|
+
status_dir = d / status
|
|
133
|
+
if status_dir.exists():
|
|
134
|
+
files.extend(status_dir.rglob("*.md"))
|
|
135
|
+
|
|
136
|
+
for f in files:
|
|
137
|
+
try:
|
|
138
|
+
meta = core.parse_issue(f, raise_error=True)
|
|
139
|
+
if meta:
|
|
140
|
+
local_id = meta.id
|
|
141
|
+
full_id = f"{project_name}::{local_id}"
|
|
142
|
+
|
|
143
|
+
if local_id in id_to_path:
|
|
144
|
+
other_path = id_to_path[local_id]
|
|
145
|
+
# Report on current file
|
|
146
|
+
d_dup = Diagnostic(
|
|
147
|
+
range=Range(
|
|
148
|
+
start=Position(line=0, character=0),
|
|
149
|
+
end=Position(line=0, character=0),
|
|
150
|
+
),
|
|
151
|
+
message=f"Duplicate ID Violation: ID '{local_id}' is already used by {other_path.name}",
|
|
152
|
+
severity=DiagnosticSeverity.Error,
|
|
153
|
+
source=local_id,
|
|
154
|
+
)
|
|
155
|
+
d_dup.data = {"path": f}
|
|
156
|
+
project_diagnostics.append(d_dup)
|
|
157
|
+
else:
|
|
158
|
+
id_to_path[local_id] = f
|
|
159
|
+
|
|
160
|
+
all_issue_ids.add(local_id)
|
|
161
|
+
all_issue_ids.add(full_id)
|
|
162
|
+
|
|
163
|
+
# Filename Consistency Check
|
|
164
|
+
# Pattern: {ID}-{slug}.md
|
|
165
|
+
expected_slug = meta.title.lower().replace(" ", "-")
|
|
166
|
+
# Remove common symbols from slug for matching
|
|
167
|
+
expected_slug = re.sub(
|
|
168
|
+
r"[^a-z0-9\-]", "", expected_slug
|
|
169
|
+
)
|
|
170
|
+
# Trim double dashes
|
|
171
|
+
expected_slug = re.sub(r"-+", "-", expected_slug).strip(
|
|
172
|
+
"-"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
filename_stem = f.stem
|
|
176
|
+
# Check if it starts with ID-
|
|
177
|
+
if not filename_stem.startswith(f"{meta.id}-"):
|
|
178
|
+
project_diagnostics.append(
|
|
179
|
+
Diagnostic(
|
|
180
|
+
range=Range(
|
|
181
|
+
start=Position(line=0, character=0),
|
|
182
|
+
end=Position(line=0, character=0),
|
|
183
|
+
),
|
|
184
|
+
message=f"Filename Error: Filename '{f.name}' must start with ID '{meta.id}-'",
|
|
185
|
+
severity=DiagnosticSeverity.Error,
|
|
186
|
+
source=meta.id,
|
|
187
|
+
data={"path": f},
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
# Check slug matching (loose match, ensuring it's present)
|
|
192
|
+
actual_slug = filename_stem[len(meta.id) + 1 :]
|
|
193
|
+
if not actual_slug:
|
|
194
|
+
project_diagnostics.append(
|
|
195
|
+
Diagnostic(
|
|
196
|
+
range=Range(
|
|
197
|
+
start=Position(line=0, character=0),
|
|
198
|
+
end=Position(line=0, character=0),
|
|
199
|
+
),
|
|
200
|
+
message=f"Filename Error: Filename '{f.name}' missing title slug. Expected: '{meta.id}-{expected_slug}.md'",
|
|
201
|
+
severity=DiagnosticSeverity.Error,
|
|
202
|
+
source=meta.id,
|
|
203
|
+
data={"path": f},
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
project_issues.append((f, meta, project_name))
|
|
208
|
+
except Exception as e:
|
|
209
|
+
# Report parsing failure as diagnostic
|
|
210
|
+
d = Diagnostic(
|
|
211
|
+
range=Range(
|
|
212
|
+
start=Position(line=0, character=0),
|
|
213
|
+
end=Position(line=0, character=0),
|
|
214
|
+
),
|
|
215
|
+
message=f"Schema Error: {str(e)}",
|
|
216
|
+
severity=DiagnosticSeverity.Error,
|
|
217
|
+
source="System",
|
|
218
|
+
)
|
|
219
|
+
d.data = {"path": f}
|
|
220
|
+
project_diagnostics.append(d)
|
|
221
|
+
return project_issues, project_diagnostics
|
|
67
222
|
|
|
68
223
|
conf = get_config(str(issues_root.parent))
|
|
69
224
|
|
|
@@ -92,7 +247,11 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
92
247
|
workspace_root_name = root_conf.project.name.lower()
|
|
93
248
|
|
|
94
249
|
# Collect from local issues_root
|
|
95
|
-
|
|
250
|
+
proj_issues, proj_diagnostics = collect_project_issues(
|
|
251
|
+
issues_root, local_project_name
|
|
252
|
+
)
|
|
253
|
+
all_issues.extend(proj_issues)
|
|
254
|
+
diagnostics.extend(proj_diagnostics)
|
|
96
255
|
|
|
97
256
|
if recursive:
|
|
98
257
|
try:
|
|
@@ -103,34 +262,76 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
|
|
|
103
262
|
if workspace_root != issues_root.parent:
|
|
104
263
|
root_issues_dir = workspace_root / "Issues"
|
|
105
264
|
if root_issues_dir.exists():
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
root_issues_dir, ws_conf.project.name.lower()
|
|
109
|
-
)
|
|
265
|
+
r_issues, r_diags = collect_project_issues(
|
|
266
|
+
root_issues_dir, ws_conf.project.name.lower()
|
|
110
267
|
)
|
|
268
|
+
all_issues.extend(r_issues)
|
|
269
|
+
diagnostics.extend(r_diags)
|
|
111
270
|
|
|
112
271
|
# Index all members
|
|
113
272
|
for member_name, rel_path in ws_conf.project.members.items():
|
|
114
273
|
member_root = (workspace_root / rel_path).resolve()
|
|
115
274
|
member_issues_dir = member_root / "Issues"
|
|
116
275
|
if member_issues_dir.exists() and member_issues_dir != issues_root:
|
|
117
|
-
|
|
118
|
-
|
|
276
|
+
m_issues, m_diags = collect_project_issues(
|
|
277
|
+
member_issues_dir, member_name.lower()
|
|
119
278
|
)
|
|
279
|
+
all_issues.extend(m_issues)
|
|
280
|
+
diagnostics.extend(m_diags)
|
|
120
281
|
except Exception:
|
|
121
282
|
pass
|
|
122
283
|
|
|
123
284
|
# 2. Validation Phase
|
|
285
|
+
valid_domains = set()
|
|
286
|
+
# Now validate
|
|
287
|
+
for path, meta, project_name in all_issues:
|
|
288
|
+
if meta == "DOMAIN":
|
|
289
|
+
valid_domains.add(
|
|
290
|
+
project_name
|
|
291
|
+
) # Record the domain name (which was stored in project_name slot)
|
|
292
|
+
|
|
124
293
|
for path, meta, project_name in all_issues:
|
|
294
|
+
if meta == "DOMAIN":
|
|
295
|
+
# Track B: Domain Validation
|
|
296
|
+
# Already did semantic checks in collection phase (H1 etc)
|
|
297
|
+
# Now do Source Language Check
|
|
298
|
+
try:
|
|
299
|
+
from monoco.features.i18n import core as i18n_core
|
|
300
|
+
|
|
301
|
+
# We need source_lang from config.
|
|
302
|
+
# We have 'conf' object from earlier.
|
|
303
|
+
source_lang = "en"
|
|
304
|
+
if conf and conf.i18n and conf.i18n.source_lang:
|
|
305
|
+
source_lang = conf.i18n.source_lang
|
|
306
|
+
|
|
307
|
+
if not i18n_core.is_content_source_language(path, source_lang):
|
|
308
|
+
diagnostics.append(
|
|
309
|
+
Diagnostic(
|
|
310
|
+
range=Range(
|
|
311
|
+
start=Position(line=0, character=0),
|
|
312
|
+
end=Position(line=0, character=0),
|
|
313
|
+
),
|
|
314
|
+
message=f"Language Mismatch: Domain definition appears not to be in source language '{source_lang}'.",
|
|
315
|
+
severity=DiagnosticSeverity.Warning,
|
|
316
|
+
source="DomainValidator",
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
# Track A: Issue Validation
|
|
125
324
|
content = path.read_text() # Re-read content for validation
|
|
126
325
|
|
|
127
326
|
# A. Run Core Validator
|
|
327
|
+
# Pass valid_domains kwarg (Validator needs update to accept it)
|
|
128
328
|
file_diagnostics = validator.validate(
|
|
129
329
|
meta,
|
|
130
330
|
content,
|
|
131
331
|
all_issue_ids,
|
|
132
332
|
current_project=project_name,
|
|
133
333
|
workspace_root=workspace_root_name,
|
|
334
|
+
valid_domains=valid_domains,
|
|
134
335
|
)
|
|
135
336
|
|
|
136
337
|
# Add context to diagnostics (Path)
|
|
@@ -183,6 +384,13 @@ def run_lint(
|
|
|
183
384
|
except Exception:
|
|
184
385
|
pass
|
|
185
386
|
|
|
387
|
+
# Collect valid domains
|
|
388
|
+
valid_domains = set()
|
|
389
|
+
domains_dir = issues_root / "Domains"
|
|
390
|
+
if domains_dir.exists():
|
|
391
|
+
for f in domains_dir.rglob("*.md"):
|
|
392
|
+
valid_domains.add(f.stem)
|
|
393
|
+
|
|
186
394
|
validator = IssueValidator(issues_root)
|
|
187
395
|
|
|
188
396
|
for file_path in file_paths:
|
|
@@ -209,7 +417,11 @@ def run_lint(
|
|
|
209
417
|
current_project_name = conf.project.name.lower()
|
|
210
418
|
|
|
211
419
|
file_diagnostics = validator.validate(
|
|
212
|
-
meta,
|
|
420
|
+
meta,
|
|
421
|
+
content,
|
|
422
|
+
all_issue_ids,
|
|
423
|
+
current_project=current_project_name,
|
|
424
|
+
valid_domains=valid_domains,
|
|
213
425
|
)
|
|
214
426
|
|
|
215
427
|
# Add context
|
|
@@ -465,12 +677,15 @@ def run_lint(
|
|
|
465
677
|
except Exception as e:
|
|
466
678
|
console.print(f"[red]Failed to fix domains for {path.name}: {e}[/red]")
|
|
467
679
|
|
|
468
|
-
# Domain Alias Fix
|
|
680
|
+
# Domain Alias and Format Fix
|
|
469
681
|
try:
|
|
470
|
-
|
|
471
|
-
d
|
|
682
|
+
format_fixes = [
|
|
683
|
+
d
|
|
684
|
+
for d in current_file_diags
|
|
685
|
+
if "Domain Format Error:" in d.message
|
|
686
|
+
or "Domain Alias:" in d.message
|
|
472
687
|
]
|
|
473
|
-
if
|
|
688
|
+
if format_fixes:
|
|
474
689
|
fm_match = re.search(
|
|
475
690
|
r"^---(.*?)---", new_content, re.DOTALL | re.MULTILINE
|
|
476
691
|
)
|
|
@@ -483,24 +698,41 @@ def run_lint(
|
|
|
483
698
|
domain_changed = False
|
|
484
699
|
if "domains" in data and isinstance(data["domains"], list):
|
|
485
700
|
domains = data["domains"]
|
|
486
|
-
for d in
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
701
|
+
for d in format_fixes:
|
|
702
|
+
if "Domain Format Error:" in d.message:
|
|
703
|
+
# Message: Domain Format Error: 'alias' must be PascalCase (e.g., 'canonical').
|
|
704
|
+
m = re.search(
|
|
705
|
+
r"Domain Format Error: '([^']+)' must be PascalCase \(e.g\., '([^']+)'\)",
|
|
706
|
+
d.message,
|
|
707
|
+
)
|
|
708
|
+
else:
|
|
709
|
+
# Message: Domain Alias: 'alias' is an alias for 'canonical'.
|
|
710
|
+
m = re.search(
|
|
711
|
+
r"Domain Alias: '([^']+)' is an alias for '([^']+)'",
|
|
712
|
+
d.message,
|
|
713
|
+
)
|
|
714
|
+
|
|
492
715
|
if m:
|
|
493
716
|
old_d = m.group(1)
|
|
494
717
|
new_d = m.group(2)
|
|
495
718
|
|
|
496
719
|
if old_d in domains:
|
|
720
|
+
# Replace exact match
|
|
497
721
|
domains = [
|
|
498
722
|
new_d if x == old_d else x for x in domains
|
|
499
723
|
]
|
|
500
724
|
domain_changed = True
|
|
501
725
|
|
|
502
726
|
if domain_changed:
|
|
503
|
-
|
|
727
|
+
# Deduplicate while preserving order if needed, but set is easier
|
|
728
|
+
seen = set()
|
|
729
|
+
unique_domains = []
|
|
730
|
+
for dom in domains:
|
|
731
|
+
if dom not in seen:
|
|
732
|
+
unique_domains.append(dom)
|
|
733
|
+
seen.add(dom)
|
|
734
|
+
|
|
735
|
+
data["domains"] = unique_domains
|
|
504
736
|
new_fm_text = yaml.dump(
|
|
505
737
|
data, sort_keys=False, allow_unicode=True
|
|
506
738
|
)
|
|
@@ -508,25 +740,17 @@ def run_lint(
|
|
|
508
740
|
fm_match.group(1), "\n" + new_fm_text
|
|
509
741
|
)
|
|
510
742
|
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
743
|
path.write_text(new_content)
|
|
520
744
|
if not any(path == p for p in processed_paths):
|
|
521
745
|
fixed_count += 1
|
|
522
746
|
processed_paths.add(path)
|
|
523
747
|
console.print(
|
|
524
|
-
f"[dim]Fixed (Domain
|
|
748
|
+
f"[dim]Fixed (Domain Normalization): {path.name}[/dim]"
|
|
525
749
|
)
|
|
526
750
|
|
|
527
751
|
except Exception as e:
|
|
528
752
|
console.print(
|
|
529
|
-
f"[red]Failed to fix domain
|
|
753
|
+
f"[red]Failed to fix domain normalization for {path.name}: {e}[/red]"
|
|
530
754
|
)
|
|
531
755
|
|
|
532
756
|
console.print(f"[green]Applied auto-fixes to {fixed_count} files.[/green]")
|
|
@@ -548,7 +772,12 @@ def run_lint(
|
|
|
548
772
|
try:
|
|
549
773
|
meta = core.parse_issue(file)
|
|
550
774
|
content = file.read_text()
|
|
551
|
-
file_diagnostics = validator.validate(
|
|
775
|
+
file_diagnostics = validator.validate(
|
|
776
|
+
meta,
|
|
777
|
+
content,
|
|
778
|
+
all_issue_ids,
|
|
779
|
+
valid_domains=valid_domains,
|
|
780
|
+
)
|
|
552
781
|
for d in file_diagnostics:
|
|
553
782
|
d.source = meta.id
|
|
554
783
|
d.data = {"path": file}
|
monoco/features/issue/models.py
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import List, Optional, Any, Dict
|
|
3
|
-
from pydantic import BaseModel, Field, model_validator, ConfigDict
|
|
3
|
+
from pydantic import BaseModel, Field, model_validator, ConfigDict, field_validator
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
import hashlib
|
|
6
6
|
import secrets
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from .criticality import CriticalityLevel, Policy, PolicyResolver
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Forward reference for type hints
|
|
13
|
+
class CommitResult:
|
|
14
|
+
"""Result of a commit operation (defined in git_service)."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
7
17
|
|
|
8
18
|
|
|
9
19
|
class IssueID:
|
|
@@ -57,6 +67,7 @@ class IssueType(str, Enum):
|
|
|
57
67
|
FEATURE = "feature"
|
|
58
68
|
CHORE = "chore"
|
|
59
69
|
FIX = "fix"
|
|
70
|
+
ARCH = "arch"
|
|
60
71
|
|
|
61
72
|
|
|
62
73
|
class IssueStatus(str, Enum):
|
|
@@ -107,7 +118,18 @@ class IssueAction(BaseModel):
|
|
|
107
118
|
class IssueMetadata(BaseModel):
|
|
108
119
|
model_config = ConfigDict(extra="allow", validate_assignment=True)
|
|
109
120
|
|
|
110
|
-
id: str
|
|
121
|
+
id: str = Field()
|
|
122
|
+
|
|
123
|
+
@field_validator("id")
|
|
124
|
+
@classmethod
|
|
125
|
+
def validate_id_format(cls, v: str) -> str:
|
|
126
|
+
if not re.match(r"^[A-Z]+-\d{4}$", v):
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Invalid Issue ID format: '{v}'. Expected 'TYPE-XXXX' (e.g., FEAT-1234). "
|
|
129
|
+
"For sub-features or sub-tasks, please use the 'parent' field instead of adding suffixes to the ID."
|
|
130
|
+
)
|
|
131
|
+
return v
|
|
132
|
+
|
|
111
133
|
uid: Optional[str] = None # Global unique identifier for cross-project identity
|
|
112
134
|
type: IssueType
|
|
113
135
|
status: IssueStatus = IssueStatus.OPEN
|
|
@@ -131,10 +153,28 @@ class IssueMetadata(BaseModel):
|
|
|
131
153
|
files: List[str] = []
|
|
132
154
|
path: Optional[str] = None # Absolute path to the issue file
|
|
133
155
|
|
|
156
|
+
# Criticality System (FEAT-0114)
|
|
157
|
+
criticality: Optional[CriticalityLevel] = Field(
|
|
158
|
+
default=None,
|
|
159
|
+
description="Issue criticality level (low, medium, high, critical)",
|
|
160
|
+
)
|
|
161
|
+
|
|
134
162
|
# Proxy UI Actions (Excluded from file persistence)
|
|
135
163
|
# Modified: Remove exclude=True to allow API/CLI inspection. Must be manually excluded during YAML Dump.
|
|
136
164
|
actions: List[IssueAction] = Field(default=[])
|
|
137
165
|
|
|
166
|
+
# Runtime-only field for commit result (FEAT-0115)
|
|
167
|
+
# Not persisted to YAML, only available in memory after update_issue
|
|
168
|
+
commit_result: Optional[Any] = Field(default=None, exclude=True)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def resolved_policy(self) -> Policy:
|
|
172
|
+
"""Get the resolved policy based on criticality level."""
|
|
173
|
+
if self.criticality:
|
|
174
|
+
return PolicyResolver.resolve(self.criticality)
|
|
175
|
+
# Default to medium policy if not set
|
|
176
|
+
return PolicyResolver.resolve(CriticalityLevel.MEDIUM)
|
|
177
|
+
|
|
138
178
|
@model_validator(mode="before")
|
|
139
179
|
@classmethod
|
|
140
180
|
def normalize_fields(cls, v: Any) -> Any:
|
|
@@ -189,6 +229,14 @@ class IssueMetadata(BaseModel):
|
|
|
189
229
|
v["stage"] = IssueStage(v["stage"])
|
|
190
230
|
except ValueError:
|
|
191
231
|
pass
|
|
232
|
+
|
|
233
|
+
# Criticality normalization
|
|
234
|
+
if "criticality" in v and isinstance(v["criticality"], str):
|
|
235
|
+
v["criticality"] = v["criticality"].lower()
|
|
236
|
+
try:
|
|
237
|
+
v["criticality"] = CriticalityLevel(v["criticality"])
|
|
238
|
+
except ValueError:
|
|
239
|
+
pass
|
|
192
240
|
return v
|
|
193
241
|
|
|
194
242
|
@model_validator(mode="after")
|