monoco-toolkit 0.1.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.
@@ -0,0 +1,178 @@
1
+ import os
2
+ import fnmatch
3
+ from pathlib import Path
4
+ from typing import List, Set, Dict, Any
5
+
6
+ DEFAULT_EXCLUDES = [".git", ".reference", "dist", "build", "node_modules", "__pycache__", ".agent", ".mono", ".venv", "venv", "ENV", "Issues"]
7
+
8
+ def load_gitignore_patterns(root: Path) -> List[str]:
9
+ """Load patterns from .gitignore file."""
10
+ gitignore_path = root / ".gitignore"
11
+ if not gitignore_path.exists():
12
+ return []
13
+
14
+ patterns = []
15
+ try:
16
+ with open(gitignore_path, "r", encoding="utf-8") as f:
17
+ for line in f:
18
+ line = line.strip()
19
+ if line and not line.startswith("#"):
20
+ # Basic normalization for fnmatch
21
+ if line.startswith("/"):
22
+ line = line[1:]
23
+ patterns.append(line)
24
+ except Exception:
25
+ pass
26
+ return patterns
27
+
28
+ def is_excluded(path: Path, root: Path, patterns: List[str]) -> bool:
29
+ """Check if a path should be excluded based on patterns and defaults."""
30
+ rel_path = str(path.relative_to(root))
31
+
32
+ # 1. Check default excludes (exact match for any path component, case-insensitive)
33
+ for part in path.parts:
34
+ if part.lower() in [e.lower() for e in DEFAULT_EXCLUDES]:
35
+ return True
36
+
37
+ # 2. Check gitignore patterns
38
+ for pattern in patterns:
39
+ # Check against relative path
40
+ if fnmatch.fnmatch(rel_path, pattern):
41
+ return True
42
+ # Check against filename
43
+ if fnmatch.fnmatch(path.name, pattern):
44
+ return True
45
+ # Check if the pattern matches a parent directory
46
+ # e.g. pattern "dist/" should match "dist/info.md"
47
+ if pattern.endswith("/"):
48
+ clean_pattern = pattern[:-1]
49
+ if rel_path.startswith(clean_pattern + "/") or rel_path == clean_pattern:
50
+ return True
51
+ elif "/" in pattern:
52
+ # If pattern has a slash, it might be a subpath match
53
+ if rel_path.startswith(pattern + "/"):
54
+ return True
55
+
56
+ return False
57
+
58
+ def discover_markdown_files(root: Path) -> List[Path]:
59
+ """Recursively find markdown files while respecting exclusion rules."""
60
+ patterns = load_gitignore_patterns(root)
61
+ all_md_files = []
62
+
63
+ # We walk to ensure we can skip directories early if needed,
64
+ # but for now rglob + filter is simpler.
65
+ for p in root.rglob("*.md"):
66
+ if p.is_file() and not is_excluded(p, root, patterns):
67
+ all_md_files.append(p)
68
+
69
+ return sorted(all_md_files)
70
+
71
+ def is_translation_file(path: Path, target_langs: List[str]) -> bool:
72
+ """Check if the given path is a translation file (target)."""
73
+ normalized_langs = [lang.lower() for lang in target_langs]
74
+
75
+ # Suffix check (case-insensitive)
76
+ stem_upper = path.stem.upper()
77
+ for lang in normalized_langs:
78
+ if stem_upper.endswith(f"_{lang.upper()}"):
79
+ return True
80
+
81
+ # Subdir check (case-insensitive)
82
+ path_parts_lower = [p.lower() for p in path.parts]
83
+ for lang in normalized_langs:
84
+ if lang in path_parts_lower:
85
+ return True
86
+
87
+ return False
88
+
89
+ def get_target_translation_path(path: Path, root: Path, lang: str) -> Path:
90
+ """Calculate the expected translation path for a specific language."""
91
+ lang = lang.lower()
92
+
93
+ # Parallel Directory Mode: docs/en/... -> docs/zh/...
94
+ # We assume 'en' is the source language for now.
95
+ path_parts = list(path.parts)
96
+ # Search for 'en' component to replace
97
+ # We iterate from root relative parts to be safe, but simple replacement of the first 'en'
98
+ # component (if not part of filename) is a good heuristic for docs structure.
99
+ for i, part in enumerate(path_parts):
100
+ if part.lower() == 'en':
101
+ path_parts[i] = lang
102
+ return Path(*path_parts)
103
+
104
+ # Suffix Mode: for root files
105
+ if path.parent == root:
106
+ return path.with_name(f"{path.stem}_{lang.upper()}{path.suffix}")
107
+
108
+ # Subdir Mode: for documentation directories (fallback)
109
+ return path.parent / lang / path.name
110
+
111
+ def check_translation_exists(path: Path, root: Path, target_langs: List[str]) -> List[str]:
112
+ """
113
+ Verify which target languages have translations.
114
+ Returns a list of missing language codes.
115
+ """
116
+ if is_translation_file(path, target_langs):
117
+ return [] # Already a translation, skip
118
+
119
+ missing = []
120
+ for lang in target_langs:
121
+ target = get_target_translation_path(path, root, lang)
122
+ if not target.exists():
123
+ missing.append(lang)
124
+ return missing
125
+ # ... (Existing code) ...
126
+
127
+ SKILL_CONTENT = """---
128
+ name: i18n-scan
129
+ description: Internationalization quality control skill.
130
+ ---
131
+
132
+ # i18n Maintenance Standard
133
+
134
+ i18n is a "first-class citizen" in Monoco.
135
+
136
+ ## Core Standards
137
+
138
+ ### 1. i18n Structure
139
+ - **Root Files**: Suffix pattern (e.g. `README_ZH.md`).
140
+ - **Docs Directories**: Subdirectory pattern (`docs/guide/zh/intro.md`).
141
+
142
+ ### 2. Exclusion Rules
143
+ - `.gitignore` (respected automatically)
144
+ - `.references/`
145
+ - Build artifacts
146
+
147
+ ## Automated Checklist
148
+ 1. **Coverage Scan**: `monoco i18n scan` - Checks missing translations.
149
+ 2. **Integrity Check**: Planned.
150
+
151
+ ## Working with I18n
152
+ - Create English docs first.
153
+ - Create translations following the naming convention.
154
+ - Run `monoco i18n scan` to verify coverage.
155
+ """
156
+
157
+ PROMPT_CONTENT = """### Documentation I18n
158
+ Manage internationalization.
159
+ - **Scan**: `monoco i18n scan` (Check for missing translations)
160
+ - **Structure**:
161
+ - Root files: `FILE_ZH.md`
162
+ - Subdirs: `folder/zh/file.md`"""
163
+
164
+ def init(root: Path):
165
+ """Initialize I18n environment (No-op currently as it relies on config)."""
166
+ # In future, could generate i18n config section if missing.
167
+ pass
168
+
169
+ def get_resources() -> Dict[str, Any]:
170
+ return {
171
+ "skills": {
172
+ "i18n": SKILL_CONTENT
173
+ },
174
+ "prompts": {
175
+ "i18n": PROMPT_CONTENT
176
+ }
177
+ }
178
+