monoco-toolkit 0.3.2__py3-none-any.whl → 0.3.3__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 CHANGED
@@ -133,6 +133,40 @@ class IssueSchemaConfig(BaseModel):
133
133
  return self
134
134
 
135
135
 
136
+ class DomainItem(BaseModel):
137
+ name: str = Field(..., description="Canonical domain name (e.g. backend.auth)")
138
+ description: Optional[str] = Field(None, description="Description of the domain")
139
+ aliases: List[str] = Field(default_factory=list, description="List of aliases")
140
+
141
+
142
+ class DomainConfig(BaseModel):
143
+ items: List[DomainItem] = Field(
144
+ default_factory=list, description="List of defined domains"
145
+ )
146
+ strict: bool = Field(
147
+ default=False, description="If True, only allow defined domains"
148
+ )
149
+
150
+ def merge(self, other: "DomainConfig") -> "DomainConfig":
151
+ if not other:
152
+ return self
153
+
154
+ # Merge items by name
155
+ if other.items:
156
+ item_map = {item.name: item for item in self.items}
157
+ for item in other.items:
158
+ # Overwrite or merge aliases? Let's overwrite for simplicity/consistency
159
+ item_map[item.name] = item
160
+ self.items = list(item_map.values())
161
+
162
+ # Strict mode: logic? maybe strict overrides?
163
+ # Let's say if ANY config asks for strict, it is strict? Or last one wins (project)?
164
+ # Default merge is usually override.
165
+ self.strict = other.strict
166
+
167
+ return self
168
+
169
+
136
170
  class StateMachineConfig(BaseModel):
137
171
  transitions: List[TransitionConfig]
138
172
 
@@ -155,6 +189,7 @@ class MonocoConfig(BaseModel):
155
189
  )
156
190
 
157
191
  issue: IssueSchemaConfig = Field(default_factory=IssueSchemaConfig)
192
+ domains: DomainConfig = Field(default_factory=DomainConfig)
158
193
 
159
194
  @staticmethod
160
195
  def _deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
@@ -134,12 +134,6 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
134
134
  bin_name="kimi",
135
135
  version_cmd="--version",
136
136
  ),
137
- "agent": AgentIntegration(
138
- key="agent",
139
- name="Antigravity",
140
- system_prompt_file="GEMINI.md",
141
- skill_root_dir=".agent/skills/",
142
- ),
143
137
  }
144
138
 
145
139
 
monoco/core/sync.py CHANGED
@@ -20,15 +20,9 @@ def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
20
20
  targets.append(cli_target)
21
21
  return targets
22
22
 
23
- # 2. Config Targets
24
- if config.agent.targets:
25
- for t in config.agent.targets:
26
- targets.append(root / t)
27
- return targets
28
-
29
- # 3. Registry Defaults (Dynamic Detection)
23
+ # 2. Registry Defaults (Dynamic Detection)
30
24
  integrations = get_active_integrations(
31
- root, config_overrides=config.agent.integrations, auto_detect=True
25
+ root, config_overrides=None, auto_detect=True
32
26
  )
33
27
 
34
28
  if integrations:
@@ -69,16 +63,9 @@ def sync_command(
69
63
  # 2. Collect Data
70
64
  collected_prompts = {}
71
65
 
72
- # Filter features based on config if specified
66
+ # Filter features based on config if specified (Deprecated: agent config removed)
73
67
  all_features = registry.get_features()
74
- active_features = []
75
-
76
- if config.agent.includes:
77
- for f in all_features:
78
- if f.name in config.agent.includes:
79
- active_features.append(f)
80
- else:
81
- active_features = all_features
68
+ active_features = all_features
82
69
 
83
70
  with console.status("[bold green]Collecting feature integration data...") as status:
84
71
  for feature in active_features:
@@ -109,7 +96,7 @@ def sync_command(
109
96
 
110
97
  # Get active integrations
111
98
  integrations = get_active_integrations(
112
- root, config_overrides=config.agent.integrations, auto_detect=True
99
+ root, config_overrides=None, auto_detect=True
113
100
  )
114
101
 
115
102
  if integrations:
@@ -231,7 +218,7 @@ def uninstall_command(
231
218
 
232
219
  # Get active integrations
233
220
  integrations = get_active_integrations(
234
- root, config_overrides=config.agent.integrations, auto_detect=True
221
+ root, config_overrides=None, auto_detect=True
235
222
  )
236
223
 
237
224
  if integrations:
@@ -17,6 +17,9 @@ backlog_app = typer.Typer(help="Manage backlog operations.")
17
17
  lsp_app = typer.Typer(help="LSP Server commands.")
18
18
  app.add_typer(backlog_app, name="backlog")
19
19
  app.add_typer(lsp_app, name="lsp")
20
+ from . import domain_commands
21
+
22
+ app.add_typer(domain_commands.app, name="domain")
20
23
  console = Console()
21
24
 
22
25
 
@@ -45,12 +48,12 @@ def create(
45
48
  ),
46
49
  sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
47
50
  tags: List[str] = typer.Option([], "--tag", help="Tags"),
51
+ domains: List[str] = typer.Option([], "--domain", help="Domains"),
48
52
  root: Optional[str] = typer.Option(
49
53
  None, "--root", help="Override issues root directory"
50
54
  ),
51
55
  json: AgentOutput = False,
52
56
  ):
53
- """Create a new issue."""
54
57
  """Create a new issue."""
55
58
  config = get_config()
56
59
  issues_root = _resolve_issues_root(config, root)
@@ -79,6 +82,7 @@ def create(
79
82
  stage=stage,
80
83
  dependencies=dependencies,
81
84
  related=related,
85
+ domains=domains,
82
86
  subdir=subdir,
83
87
  sprint=sprint,
84
88
  tags=tags,
@@ -99,6 +103,25 @@ def create(
99
103
  )
100
104
  console.print(f"Path: {rel_path}")
101
105
 
106
+ # Prompt for Language
107
+ target_langs = config.i18n.target_langs
108
+ primary_lang = target_langs[0] if target_langs else "en"
109
+
110
+ # Simple mapping for display
111
+ lang_display = {
112
+ "zh": "Chinese (Simplified)",
113
+ "en": "English",
114
+ "ja": "Japanese",
115
+ }.get(primary_lang, primary_lang)
116
+
117
+ console.print(
118
+ f"\n[bold yellow]Agent Hint:[/bold yellow] Please fill the ticket content in [bold cyan]{lang_display}[/bold cyan]."
119
+ )
120
+
121
+ except ValueError as e:
122
+ OutputManager.error(str(e))
123
+ raise typer.Exit(code=1)
124
+
102
125
  except ValueError as e:
103
126
  OutputManager.error(str(e))
104
127
  raise typer.Exit(code=1)
@@ -53,28 +53,108 @@ def _get_slug(title: str) -> str:
53
53
  return slug
54
54
 
55
55
 
56
- def parse_issue(file_path: Path) -> Optional[IssueMetadata]:
56
+ def parse_issue(file_path: Path, raise_error: bool = False) -> Optional[IssueMetadata]:
57
57
  if not file_path.suffix == ".md":
58
58
  return None
59
59
 
60
60
  content = file_path.read_text()
61
61
  match = re.search(r"^---(.*?)---", content, re.DOTALL | re.MULTILINE)
62
62
  if not match:
63
+ if raise_error:
64
+ raise ValueError(f"No frontmatter found in {file_path.name}")
63
65
  return None
64
66
 
65
67
  try:
66
68
  data = yaml.safe_load(match.group(1))
67
69
  if not isinstance(data, dict):
70
+ if raise_error:
71
+ raise ValueError(f"Frontmatter is not a dictionary in {file_path.name}")
68
72
  return None
69
73
 
70
74
  data["path"] = str(file_path.absolute())
71
75
  meta = IssueMetadata(**data)
72
76
  meta.actions = get_available_actions(meta)
73
77
  return meta
74
- except Exception:
78
+ except Exception as e:
79
+ if raise_error:
80
+ raise e
75
81
  return None
76
82
 
77
83
 
84
+ def _serialize_metadata(metadata: IssueMetadata) -> str:
85
+ """
86
+ Centralized serialization logic to ensure explicit fields and correct ordering.
87
+ """
88
+ # Serialize metadata
89
+ # We want explicit fields even if None/Empty to enforce schema awareness
90
+ data = metadata.model_dump(
91
+ exclude_none=True, mode="json", exclude={"actions", "path"}
92
+ )
93
+
94
+ # Force explicit keys if missing (due to exclude_none or defaults)
95
+ if "parent" not in data:
96
+ data["parent"] = None
97
+ if "dependencies" not in data:
98
+ data["dependencies"] = []
99
+ if "related" not in data:
100
+ data["related"] = []
101
+ if "domains" not in data:
102
+ data["domains"] = []
103
+ if "files" not in data:
104
+ data["files"] = []
105
+
106
+ # Custom YAML Dumper to preserve None as 'null' and order
107
+ # Helper to order keys: id, uid, type, status, stage, title, ... graph ...
108
+ # Simple sort isn't enough, we rely on insertion order (Python 3.7+)
109
+ ordered_data = {
110
+ k: data[k]
111
+ for k in [
112
+ "id",
113
+ "uid",
114
+ "type",
115
+ "status",
116
+ "stage",
117
+ "title",
118
+ "created_at",
119
+ "updated_at",
120
+ ]
121
+ if k in data
122
+ }
123
+ # Add graph fields
124
+ for k in [
125
+ "priority",
126
+ "parent",
127
+ "dependencies",
128
+ "related",
129
+ "domains",
130
+ "tags",
131
+ "files",
132
+ ]:
133
+ if k in data:
134
+ ordered_data[k] = data[k]
135
+ elif k in ["dependencies", "related", "domains", "tags", "files"]:
136
+ ordered_data[k] = []
137
+ elif k == "parent":
138
+ ordered_data[k] = None
139
+
140
+ # Add remaining
141
+ for k, v in data.items():
142
+ if k not in ordered_data:
143
+ ordered_data[k] = v
144
+
145
+ yaml_header = yaml.dump(
146
+ ordered_data, sort_keys=False, allow_unicode=True, default_flow_style=False
147
+ )
148
+
149
+ # Inject Comments for guidance (replace keys with key+comment)
150
+ if "parent" in ordered_data and ordered_data["parent"] is None:
151
+ yaml_header = yaml_header.replace(
152
+ "parent: null", "parent: null # <EPIC-ID> Optional"
153
+ )
154
+
155
+ return yaml_header
156
+
157
+
78
158
  def parse_issue_detail(file_path: Path) -> Optional[IssueDetail]:
79
159
  if not file_path.suffix == ".md":
80
160
  return None
@@ -127,6 +207,7 @@ def create_issue_file(
127
207
  stage: Optional[IssueStage] = None,
128
208
  dependencies: List[str] = [],
129
209
  related: List[str] = [],
210
+ domains: List[str] = [],
130
211
  subdir: Optional[str] = None,
131
212
  sprint: Optional[str] = None,
132
213
  tags: List[str] = [],
@@ -149,8 +230,7 @@ def create_issue_file(
149
230
 
150
231
  target_dir.mkdir(parents=True, exist_ok=True)
151
232
 
152
- # Auto-Populate Tags with required IDs (Requirement: Maintain tags field with parent/deps/related/self IDs)
153
- # Ensure they are prefixed with '#' for tagging convention if not present (usually tags are just strings, but user asked for #ID)
233
+ # Auto-Populate Tags with required IDs
154
234
  auto_tags = set(tags) if tags else set()
155
235
 
156
236
  # 1. Add Parent
@@ -165,15 +245,14 @@ def create_issue_file(
165
245
  for rel in related:
166
246
  auto_tags.add(f"#{rel}")
167
247
 
168
- # 4. Add Self (as per instruction "auto add this issue... number")
169
- # Note: issue_id is generated just above
248
+ # 4. Add Self
170
249
  auto_tags.add(f"#{issue_id}")
171
250
 
172
251
  final_tags = sorted(list(auto_tags))
173
252
 
174
253
  metadata = IssueMetadata(
175
254
  id=issue_id,
176
- uid=generate_uid(), # Generate global unique identifier
255
+ uid=generate_uid(),
177
256
  type=issue_type,
178
257
  status=status,
179
258
  stage=stage,
@@ -181,43 +260,23 @@ def create_issue_file(
181
260
  parent=parent,
182
261
  dependencies=dependencies,
183
262
  related=related,
263
+ domains=domains,
184
264
  sprint=sprint,
185
265
  tags=final_tags,
186
266
  opened_at=current_time() if status == IssueStatus.OPEN else None,
187
267
  )
188
268
 
189
- # Enforce lifecycle policies (defaults, auto-corrections)
269
+ # Enforce lifecycle policies
190
270
  from .engine import get_engine
191
271
 
192
272
  get_engine().enforce_policy(metadata)
193
273
 
194
274
  # Serialize metadata
195
- # Explicitly exclude actions and path from file persistence
196
- yaml_header = yaml.dump(
197
- metadata.model_dump(
198
- exclude_none=True, mode="json", exclude={"actions", "path"}
199
- ),
200
- sort_keys=False,
201
- allow_unicode=True,
202
- )
203
-
204
- # Inject Self-Documenting Hints (Interactive Frontmatter)
205
- if "parent:" not in yaml_header:
206
- yaml_header += "# parent: <EPIC-ID> # Optional: Parent Issue ID\n"
207
- if "solution:" not in yaml_header:
208
- yaml_header += "# solution: null # Required for Closed state (implemented, cancelled, etc.)\n"
209
-
210
- if "dependencies:" not in yaml_header:
211
- yaml_header += "# dependencies: [] # List of dependency IDs\n"
212
- if "related:" not in yaml_header:
213
- yaml_header += "# related: [] # List of related issue IDs\n"
214
- if "files:" not in yaml_header:
215
- yaml_header += "# files: [] # List of modified files\n"
275
+ yaml_header = _serialize_metadata(metadata)
216
276
 
217
277
  slug = _get_slug(title)
218
278
  filename = f"{issue_id}-{slug}.md"
219
279
 
220
- # Enhanced Template with Instructional Comments
221
280
  file_content = f"""---
222
281
  {yaml_header}---
223
282
 
@@ -249,7 +308,6 @@ def create_issue_file(
249
308
  file_path = target_dir / filename
250
309
  file_path.write_text(file_content)
251
310
 
252
- # Inject path into returned metadata
253
311
  metadata.path = str(file_path.absolute())
254
312
 
255
313
  return metadata, file_path
@@ -512,14 +570,7 @@ def update_issue(
512
570
  raise ValueError(f"Failed to validate updated metadata: {e}")
513
571
 
514
572
  # Serialize back
515
- # Explicitly exclude actions and path from file persistence
516
- new_yaml = yaml.dump(
517
- updated_meta.model_dump(
518
- exclude_none=True, mode="json", exclude={"actions", "path"}
519
- ),
520
- sort_keys=False,
521
- allow_unicode=True,
522
- )
573
+ new_yaml = _serialize_metadata(updated_meta)
523
574
 
524
575
  # Reconstruct File
525
576
  match_header = re.search(r"^---(.*?)---", content, re.DOTALL | re.MULTILINE)
@@ -113,6 +113,7 @@ class IssueFrontmatter(BaseModel):
113
113
  parent: Optional[str] = None
114
114
  dependencies: List[str] = Field(default_factory=list)
115
115
  related: List[str] = Field(default_factory=list)
116
+ domains: List[str] = Field(default_factory=list)
116
117
  tags: List[str] = Field(default_factory=list)
117
118
  solution: Optional[IssueSolution] = None
118
119
  isolation: Optional[IssueIsolation] = None
@@ -0,0 +1,47 @@
1
+ import typer
2
+ from rich.table import Table
3
+ from rich.console import Console
4
+ from monoco.features.issue.domain_service import DomainService
5
+
6
+ app = typer.Typer(help="Manage domain ontology.")
7
+ console = Console()
8
+
9
+
10
+ @app.command("list")
11
+ def list_domains():
12
+ """List defined domains and aliases."""
13
+ service = DomainService()
14
+ config = service.config
15
+
16
+ table = Table(title=f"Domain Ontology (Strict: {config.strict})")
17
+ table.add_column("Canonical Name", style="bold cyan")
18
+ table.add_column("Description", style="white")
19
+ table.add_column("Aliases", style="yellow")
20
+
21
+ for item in config.items:
22
+ table.add_row(
23
+ item.name,
24
+ item.description or "",
25
+ ", ".join(item.aliases) if item.aliases else "-",
26
+ )
27
+
28
+ console.print(table)
29
+
30
+
31
+ @app.command("check")
32
+ def check_domain(domain: str = typer.Argument(..., help="Domain name to check")):
33
+ """Check if a domain is valid and resolve it."""
34
+ service = DomainService()
35
+
36
+ if service.is_canonical(domain):
37
+ console.print(f"[green]✔ '{domain}' is a canonical domain.[/green]")
38
+ elif service.is_alias(domain):
39
+ canonical = service.get_canonical(domain)
40
+ console.print(f"[yellow]➜ '{domain}' is an alias for '{canonical}'.[/yellow]")
41
+ else:
42
+ if service.config.strict:
43
+ console.print(f"[red]✘ '{domain}' is NOT a valid domain.[/red]")
44
+ else:
45
+ console.print(
46
+ f"[yellow]⚠ '{domain}' is undefined (Strict Mode: OFF).[/yellow]"
47
+ )
@@ -0,0 +1,69 @@
1
+ from typing import Dict, Optional, Set
2
+ from monoco.core.config import get_config, DomainConfig
3
+
4
+
5
+ class DomainService:
6
+ """
7
+ Service for managing domain ontology, aliases, and validation.
8
+ """
9
+
10
+ def __init__(self, config: Optional[DomainConfig] = None):
11
+ self.config = config or get_config().domains
12
+ self._alias_map: Dict[str, str] = {}
13
+ self._canonical_domains: Set[str] = set()
14
+ self._build_index()
15
+
16
+ def _build_index(self):
17
+ self._alias_map.clear()
18
+ self._canonical_domains.clear()
19
+
20
+ for item in self.config.items:
21
+ self._canonical_domains.add(item.name)
22
+ for alias in item.aliases:
23
+ self._alias_map[alias] = item.name
24
+
25
+ def reload(self):
26
+ """Reload configuration (if get_config returns new instance referenced)"""
27
+ # Usually get_config() returns the singleton. If singleton updates, we might see it?
28
+ # But we stored self.config.
29
+ # Ideally we fetch fresh config if we want reload.
30
+ self.config = get_config().domains
31
+ self._build_index()
32
+
33
+ def is_defined(self, domain: str) -> bool:
34
+ """Check if domain is known (canonical or alias)."""
35
+ return domain in self._canonical_domains or domain in self._alias_map
36
+
37
+ def is_canonical(self, domain: str) -> bool:
38
+ """Check if domain is a canonical name."""
39
+ return domain in self._canonical_domains
40
+
41
+ def is_alias(self, domain: str) -> bool:
42
+ """Check if domain is a known alias."""
43
+ return domain in self._alias_map
44
+
45
+ def get_canonical(self, domain: str) -> Optional[str]:
46
+ """
47
+ Resolve alias to canonical name.
48
+ Returns Canonical Name if found.
49
+ Returns None if it is not an alias (could be canonical or unknown).
50
+ """
51
+ return self._alias_map.get(domain)
52
+
53
+ def normalize(self, domain: str) -> str:
54
+ """
55
+ Normalize domain: return canonical if it's an alias, else return original.
56
+ """
57
+ return self._alias_map.get(domain, domain)
58
+
59
+ def suggest_correction(self, domain: str) -> Optional[str]:
60
+ """
61
+ Suggest a correction for an unknown domain (Fuzzy matching).
62
+ """
63
+ # Simple fuzzy match implementation (optional)
64
+ # Using simple containment or levenshtein if available?
65
+ # Let's keep it simple: check if domain is substring of canonical?
66
+ # Or simple typo check loop.
67
+
68
+ # For now, just return None as fuzzy match is optional and requires dependency or complex logic
69
+ return None
@@ -7,7 +7,7 @@ import re
7
7
  from monoco.core import git
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
 
@@ -68,15 +68,29 @@ def check_integrity(issues_root: Path, recursive: bool = False) -> List[Diagnost
68
68
  files.extend(status_dir.rglob("*.md"))
69
69
 
70
70
  for f in files:
71
- meta = core.parse_issue(f)
72
- if meta:
73
- local_id = meta.id
74
- full_id = f"{project_name}::{local_id}"
75
-
76
- all_issue_ids.add(local_id)
77
- all_issue_ids.add(full_id)
78
-
79
- project_issues.append((f, meta))
71
+ try:
72
+ meta = core.parse_issue(f, raise_error=True)
73
+ if meta:
74
+ local_id = meta.id
75
+ full_id = f"{project_name}::{local_id}"
76
+
77
+ all_issue_ids.add(local_id)
78
+ all_issue_ids.add(full_id)
79
+
80
+ project_issues.append((f, meta))
81
+ except Exception as e:
82
+ # Report parsing failure as diagnostic
83
+ d = Diagnostic(
84
+ range=Range(
85
+ start=Position(line=0, character=0),
86
+ end=Position(line=0, character=0),
87
+ ),
88
+ message=f"Schema Error: {str(e)}",
89
+ severity=DiagnosticSeverity.Error,
90
+ source="System",
91
+ )
92
+ d.data = {"path": f}
93
+ diagnostics.append(d)
80
94
  return project_issues
81
95
 
82
96
  from monoco.core.config import get_config
@@ -193,7 +207,7 @@ def run_lint(
193
207
 
194
208
  # Parse and validate file
195
209
  try:
196
- meta = core.parse_issue(file)
210
+ meta = core.parse_issue(file, raise_error=True)
197
211
  if not meta:
198
212
  console.print(
199
213
  f"[yellow]Warning:[/yellow] Failed to parse issue metadata from {file_path}. Skipping."
@@ -315,6 +329,36 @@ def run_lint(
315
329
  new_content = "\n".join(lines) + "\n"
316
330
  has_changes = True
317
331
 
332
+ if (
333
+ "Hierarchy Violation" in d.message
334
+ and "Epics must have a parent" in d.message
335
+ ):
336
+ try:
337
+ fm_match = re.search(
338
+ r"^---(.*?)---", new_content, re.DOTALL | re.MULTILINE
339
+ )
340
+ if fm_match:
341
+ import yaml
342
+
343
+ fm_text = fm_match.group(1)
344
+ data = yaml.safe_load(fm_text) or {}
345
+
346
+ # Default to EPIC-0000
347
+ data["parent"] = "EPIC-0000"
348
+
349
+ new_fm_text = yaml.dump(
350
+ data, sort_keys=False, allow_unicode=True
351
+ )
352
+ # Replace FM block
353
+ new_content = new_content.replace(
354
+ fm_match.group(1), "\n" + new_fm_text
355
+ )
356
+ has_changes = True
357
+ except Exception as ex:
358
+ console.print(
359
+ f"[red]Failed to fix parent hierarchy: {ex}[/red]"
360
+ )
361
+
318
362
  if "Tag Check: Missing required context tags" in d.message:
319
363
  # Extract missing tags from message
320
364
  # Message format: "Tag Check: Missing required context tags: #TAG1, #TAG2"
@@ -426,6 +470,70 @@ def run_lint(
426
470
  except Exception as e:
427
471
  console.print(f"[red]Failed to fix domains for {path.name}: {e}[/red]")
428
472
 
473
+ # Domain Alias Fix
474
+ try:
475
+ alias_fixes = [
476
+ d for d in current_file_diags if "Domain Alias:" in d.message
477
+ ]
478
+ if alias_fixes:
479
+ fm_match = re.search(
480
+ r"^---(.*?)---", new_content, re.DOTALL | re.MULTILINE
481
+ )
482
+ if fm_match:
483
+ import yaml
484
+
485
+ fm_text = fm_match.group(1)
486
+ data = yaml.safe_load(fm_text) or {}
487
+
488
+ domain_changed = False
489
+ if "domains" in data and isinstance(data["domains"], list):
490
+ domains = data["domains"]
491
+ for d in alias_fixes:
492
+ # Parse message: Domain Alias: 'alias' is an alias for 'canonical'.
493
+ m = re.search(
494
+ r"Domain Alias: '([^']+)' is an alias for '([^']+)'",
495
+ d.message,
496
+ )
497
+ if m:
498
+ old_d = m.group(1)
499
+ new_d = m.group(2)
500
+
501
+ if old_d in domains:
502
+ domains = [
503
+ new_d if x == old_d else x for x in domains
504
+ ]
505
+ domain_changed = True
506
+
507
+ if domain_changed:
508
+ data["domains"] = domains
509
+ new_fm_text = yaml.dump(
510
+ data, sort_keys=False, allow_unicode=True
511
+ )
512
+ new_content = new_content.replace(
513
+ fm_match.group(1), "\n" + new_fm_text
514
+ )
515
+ has_changes = True
516
+
517
+ # Write immediately if not handled by previous block?
518
+ # We are in standard flow where has_changes flag handles write at end of loop?
519
+ # Wait, the previous block (Missing domains) logic wrote internally ONLY if has_changes.
520
+ # AND it reset has_changes=False at start of try?
521
+ # Actually the previous block structure was separate try-except blocks.
522
+ # But here I am inserting AFTER the Missing Domains try-except (which was lines 390-442).
523
+ # But I need to write if I changed it.
524
+ path.write_text(new_content)
525
+ if not any(path == p for p in processed_paths):
526
+ fixed_count += 1
527
+ processed_paths.add(path)
528
+ console.print(
529
+ f"[dim]Fixed (Domain Alias): {path.name}[/dim]"
530
+ )
531
+
532
+ except Exception as e:
533
+ console.print(
534
+ f"[red]Failed to fix domain aliases for {path.name}: {e}[/red]"
535
+ )
536
+
429
537
  console.print(f"[green]Applied auto-fixes to {fixed_count} files.[/green]")
430
538
 
431
539
  # Re-run validation to verify