ki-manager 2.0.0__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.
ki_manager/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """ki-manager — Knowledge Item MCP server for AI-assisted development."""
2
+ __version__ = "2.0.0"
@@ -0,0 +1,57 @@
1
+ """
2
+ add_ki_to_config.py
3
+
4
+ CLI helper: registers a new Knowledge Item in doc_config.json.
5
+
6
+ Usage:
7
+ python add_ki_to_config.py <ki_name> <description> <covers_json> <depends_on_json>
8
+
9
+ Example:
10
+ python add_ki_to_config.py KI_utils.md "Utility helpers" '["Utilities"]' '["src/utils/"]'
11
+ """
12
+
13
+ import sys
14
+ import os
15
+ import json
16
+
17
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
+ import ki_utils
19
+
20
+
21
+ def add_ki(ki_name: str, description: str, covers: list, depends_on: list):
22
+ config = ki_utils.get_doc_config()
23
+ if not config:
24
+ print("[!] doc_config.json not found or empty")
25
+ return
26
+
27
+ if "knowledge_items" not in config:
28
+ config["knowledge_items"] = {}
29
+
30
+ config["knowledge_items"][ki_name] = {
31
+ "description": description,
32
+ "covers": covers,
33
+ "depends_on": depends_on,
34
+ }
35
+
36
+ with open(ki_utils.get_doc_config_path(), "w", encoding="utf-8") as f:
37
+ json.dump(config, f, indent=4, ensure_ascii=False)
38
+
39
+ print(f"[+] KI registered in doc_config.json: {ki_name}")
40
+
41
+
42
+ if __name__ == "__main__":
43
+ if len(sys.argv) < 5:
44
+ print("Usage: python add_ki_to_config.py <ki_name> <description> "
45
+ "<covers_list_json> <depends_on_list_json>")
46
+ sys.exit(1)
47
+
48
+ ki_name = sys.argv[1]
49
+ description = sys.argv[2]
50
+ try:
51
+ covers = json.loads(sys.argv[3])
52
+ depends_on = json.loads(sys.argv[4])
53
+ except json.JSONDecodeError:
54
+ print("[!] Error: covers and depends_on must be valid JSON arrays")
55
+ sys.exit(1)
56
+
57
+ add_ki(ki_name, description, covers, depends_on)
@@ -0,0 +1,103 @@
1
+ import os
2
+ import sys
3
+ import argparse
4
+ import io
5
+ from typing import Dict, List, Optional
6
+
7
+ # Добавляем путь к скриптам, чтобы импортировать ki_utils
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+ import ki_utils
10
+
11
+ def format_size(size_bytes: int) -> str:
12
+ if size_bytes < 1024:
13
+ return f"{size_bytes} B"
14
+ elif size_bytes < 1024 * 1024:
15
+ return f"{size_bytes / 1024:.2f} KB"
16
+ else:
17
+ return f"{size_bytes / (1024 * 1024):.2f} MB"
18
+
19
+ def get_tracked_files(config: Dict) -> Dict[str, str]:
20
+ """Возвращает мапу {путь_к_файлу: имя_KI}"""
21
+ tracked = {}
22
+ ki_items = config.get("knowledge_items", {})
23
+ for ki_name, ki_data in ki_items.items():
24
+ for file_path in ki_data.get("depends_on", []):
25
+ tracked[os.path.normpath(file_path)] = ki_name
26
+ return tracked
27
+
28
+ def analyze_path(target_path: str, recursive: bool = False):
29
+ project_root = ki_utils.get_project_root()
30
+ config = ki_utils.get_doc_config()
31
+ tracked_map = get_tracked_files(config)
32
+
33
+ # Резолвим абсолютный путь
34
+ abs_target = os.path.normpath(os.path.join(project_root, target_path))
35
+
36
+ if not os.path.exists(abs_target):
37
+ print(f"Error: Path {target_path} not found.")
38
+ return
39
+
40
+ print(f"### Analysis for: `{target_path}`")
41
+ print("| Status | File / Directory | Size | KI Link | Density |")
42
+ print("|:---:|:---|:---|:---|:---|")
43
+
44
+ items = []
45
+ if os.path.isfile(abs_target):
46
+ items = [abs_target]
47
+ elif os.path.isdir(abs_target):
48
+ if recursive:
49
+ for root, dirs, files in os.walk(abs_target):
50
+ if any(part in root.split(os.sep) for part in ["__pycache__", ".git", "node_modules", "venv", ".venv"]):
51
+ continue
52
+ for name in files:
53
+ items.append(os.path.join(root, name))
54
+ else:
55
+ for name in os.listdir(abs_target):
56
+ items.append(os.path.join(abs_target, name))
57
+
58
+ for item_path in sorted(items):
59
+ try:
60
+ rel_path = os.path.relpath(item_path, project_root)
61
+ is_dir = os.path.isdir(item_path)
62
+ size = 0
63
+ if is_dir:
64
+ for root, _, files in os.walk(item_path):
65
+ for f in files:
66
+ fp = os.path.join(root, f)
67
+ if os.path.exists(fp):
68
+ size += os.path.getsize(fp)
69
+ else:
70
+ size = os.path.getsize(item_path)
71
+
72
+ ki_name = tracked_map.get(os.path.normpath(rel_path), "")
73
+ status = "✅" if ki_name else "❌"
74
+
75
+ density = "-"
76
+ if ki_name:
77
+ ki_path = os.path.join(ki_utils.get_knowledge_root(), "knowledge", ki_name)
78
+ if os.path.exists(ki_path):
79
+ ki_size = os.path.getsize(ki_path)
80
+ if ki_size > 0:
81
+ density_val = size / ki_size
82
+ density = f"{density_val:.1f} B_code/B_doc"
83
+
84
+ name_display = f"`{os.path.basename(item_path)}`" + ("/" if is_dir else "")
85
+ print(f"| {status} | {name_display} | {format_size(size)} | {ki_name} | {density} |")
86
+ except:
87
+ continue
88
+
89
+ def main():
90
+ # Исправление кодировки для Windows при прямом запуске
91
+ if sys.platform == "win32":
92
+ import io
93
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
94
+
95
+ parser = argparse.ArgumentParser(description="Analyze module statistics with knowledge context.")
96
+ parser.add_argument("path", help="Path relative to project root")
97
+ parser.add_argument("--recursive", action="store_true", help="Recursive analysis")
98
+
99
+ args = parser.parse_args()
100
+ analyze_path(args.path, args.recursive)
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,406 @@
1
+ """
2
+ audit_coverage.py
3
+
4
+ Audit of Knowledge Base coverage for the project.
5
+
6
+ Compares:
7
+ - Project directories tracked in doc_config.json
8
+ - Knowledge Items registered in doc_config.json
9
+
10
+ Outputs a coverage matrix with priority recommendations for /expand-knowledge.
11
+ Saves the result to <knowledge_root>/coverage_matrix.md
12
+
13
+ Usage:
14
+ .venv/Scripts/python.exe scripts/audit_coverage.py
15
+ .venv/Scripts/python.exe scripts/audit_coverage.py --root /path/to/project
16
+ .venv/Scripts/python.exe scripts/audit_coverage.py --no-save
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ import json
22
+ import argparse
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+
26
+ # Add scripts dir to path so ki_utils is importable
27
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
28
+ import ki_utils
29
+
30
+ # ─── Configuration ────────────────────────────────────────────────────────────
31
+
32
+ def get_knowledge_root():
33
+ return ki_utils.get_knowledge_root()
34
+
35
+
36
+ def get_project_root():
37
+ return ki_utils.get_project_root()
38
+
39
+
40
+ # ─── Data Loading ─────────────────────────────────────────────────────────────
41
+
42
+ def load_tracked_modules():
43
+ doc_config = ki_utils.get_doc_config()
44
+ return doc_config.get("coverage_settings", {}).get("tracked_modules", [])
45
+
46
+
47
+ def load_doc_config() -> dict:
48
+ """Loads doc_config.json."""
49
+ return ki_utils.get_doc_config()
50
+
51
+
52
+ def load_ki_files() -> list:
53
+ """Returns a list of KI file names."""
54
+ ki_dir = os.path.join(get_knowledge_root(), "knowledge")
55
+ if not os.path.isdir(ki_dir):
56
+ return []
57
+ return [f for f in os.listdir(ki_dir) if f.startswith("KI_") and f.endswith(".md")]
58
+
59
+
60
+ # ─── Metrics ──────────────────────────────────────────────────────────────────
61
+
62
+
63
+
64
+ def get_module_files(project_root: str, module_path: str) -> list:
65
+ abs_module = os.path.join(project_root, module_path.replace("/", os.sep))
66
+ if not os.path.isdir(abs_module):
67
+ return []
68
+ files_list = []
69
+ for root, dirs, files in os.walk(abs_module):
70
+ dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
71
+ for f in files:
72
+ if f.endswith((".py", ".ts", ".tsx", ".js", ".jsx")) and not f.startswith("__"):
73
+ rel_path = os.path.relpath(os.path.join(root, f), project_root)
74
+ files_list.append(rel_path)
75
+ return files_list
76
+
77
+
78
+ def get_covered_paths(doc_config: dict) -> set:
79
+ paths = set()
80
+ for entry in doc_config.get("knowledge_items", {}).values():
81
+ for p in entry.get("depends_on", []):
82
+ paths.add(p.replace("/", os.sep).rstrip(os.sep))
83
+ return paths
84
+
85
+
86
+ def is_path_covered(file_path: str, covered_paths: set) -> bool:
87
+ """Checks if a path is covered by any of the registered KI dependencies."""
88
+ file_norm = file_path.replace("/", os.sep).lower()
89
+ for cp in covered_paths:
90
+ cp_norm = cp.replace("/", os.sep).lower()
91
+ if file_norm == cp_norm or file_norm.startswith(cp_norm + os.sep):
92
+ return True
93
+ return False
94
+
95
+
96
+ def has_ki_coverage(module_path: str, ki_files: list, doc_config: dict) -> bool:
97
+ module_norm = module_path.replace("/", os.sep).rstrip(os.sep) + os.sep
98
+ for ki_name, entry in doc_config.get("knowledge_items", {}).items():
99
+ for dep in entry.get("depends_on", []):
100
+ dep_norm = dep.replace("/", os.sep)
101
+ if dep_norm.startswith(module_norm):
102
+ return True
103
+ return False
104
+
105
+
106
+ def get_ki_size(project_root: str, module_path: str, doc_config: dict) -> int:
107
+ module_norm = module_path.replace("/", os.sep).rstrip(os.sep)
108
+ total_size = 0
109
+ ki_dir = os.path.join(get_knowledge_root(), "knowledge")
110
+
111
+ for ki_name, entry in doc_config.get("knowledge_items", {}).items():
112
+ covered = any(
113
+ dep.replace("/", os.sep).startswith(module_norm)
114
+ for dep in entry.get("depends_on", [])
115
+ )
116
+ if covered:
117
+ ki_path = os.path.join(ki_dir, ki_name)
118
+ if os.path.exists(ki_path):
119
+ total_size += os.path.getsize(ki_path)
120
+ return total_size
121
+
122
+
123
+ def find_untracked_dirs(project_root: str, tracked_modules: list) -> list:
124
+ tracked_paths = {m[0].replace("/", os.sep).rstrip(os.sep) for m in tracked_modules}
125
+ untracked = []
126
+ for item in os.listdir(project_root):
127
+ abs_item = os.path.join(project_root, item)
128
+ if not os.path.isdir(abs_item) or item in EXCLUDED_DIRS or item.startswith("."):
129
+ continue
130
+ for root, dirs, files in os.walk(abs_item):
131
+ dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS and not d.startswith(".")]
132
+ rel_path = os.path.relpath(root, project_root)
133
+ norm_rel = rel_path.replace("/", os.sep).rstrip(os.sep)
134
+ if any(not f.startswith(".") for f in files):
135
+ is_sub_covered = any(
136
+ norm_rel == tp or norm_rel.startswith(tp + os.sep)
137
+ for tp in tracked_paths
138
+ )
139
+ if not is_sub_covered:
140
+ untracked.append(rel_path)
141
+ dirs[:] = []
142
+ return untracked
143
+
144
+
145
+ def priority_label(priority: int) -> str:
146
+ if priority >= 8:
147
+ return "🔴 Critical"
148
+ elif priority >= 5:
149
+ return "🟡 Medium"
150
+ else:
151
+ return "🟢 Low"
152
+
153
+
154
+ # ─── Matrix Builder ────────────────────────────────────────────────────────────
155
+
156
+ def build_coverage_matrix(project_root: str, tracked_modules: list) -> dict:
157
+ doc_config = load_doc_config()
158
+ ki_files = load_ki_files()
159
+ covered_paths = get_covered_paths(doc_config)
160
+
161
+ ki_complexity = {
162
+ ki_name: len(entry.get("depends_on", []))
163
+ for ki_name, entry in doc_config.get("knowledge_items", {}).items()
164
+ }
165
+
166
+ rows = []
167
+ for module_path, label, importance in tracked_modules:
168
+ module_files = get_module_files(project_root, module_path)
169
+ total_files = len(module_files)
170
+
171
+ module_total_bytes = 0
172
+ module_covered_bytes = 0
173
+ for f in module_files:
174
+ abs_f = os.path.join(project_root, f)
175
+ try:
176
+ f_size = os.path.getsize(abs_f)
177
+ module_total_bytes += f_size
178
+ if is_path_covered(f, covered_paths):
179
+ module_covered_bytes += f_size
180
+ except OSError:
181
+ continue
182
+
183
+ size_kb = round(module_total_bytes / 1024, 1)
184
+ covered_size_kb = round(module_covered_bytes / 1024, 1)
185
+
186
+ if module_total_bytes == 0:
187
+ coverage_pct = 100.0 if os.path.exists(os.path.join(project_root, module_path)) else 0.0
188
+ else:
189
+ coverage_pct = (module_covered_bytes / module_total_bytes) * 100
190
+
191
+ has_ki = has_ki_coverage(module_path, ki_files, doc_config)
192
+ ki_size = get_ki_size(project_root, module_path, doc_config)
193
+ density = round(ki_size / size_kb, 1) if size_kb > 0 else 0.0
194
+
195
+ complex_kis = [
196
+ k for k, v in ki_complexity.items()
197
+ if v > COMPLEXITY_THRESHOLD and any(
198
+ dep.startswith(module_path)
199
+ for dep in doc_config["knowledge_items"][k].get("depends_on", [])
200
+ )
201
+ ]
202
+
203
+ gap_factor = 1.0 + (100 - coverage_pct) / 50.0
204
+ if not has_ki:
205
+ gap_factor += 1.0
206
+ density_factor = 1.5 if (has_ki and size_kb > 5 and density < DENSITY_THRESHOLD) else 1.0
207
+ computed_priority = round(importance * gap_factor * (1 + size_kb / 200) * density_factor)
208
+
209
+ rows.append({
210
+ "module": module_path,
211
+ "label": label,
212
+ "files": total_files,
213
+ "size_kb": size_kb,
214
+ "covered_size_kb": covered_size_kb,
215
+ "has_ki": has_ki,
216
+ "ki_size": ki_size,
217
+ "density": density,
218
+ "coverage_pct": round(coverage_pct, 1),
219
+ "importance": importance,
220
+ "priority": computed_priority,
221
+ "complex_kis": complex_kis,
222
+ "status": "✅ Covered" if coverage_pct == 100 else
223
+ "⚠️ Partial" if coverage_pct > 0 else "❌ Not Covered"
224
+ })
225
+
226
+ rows.sort(key=lambda r: (-r["priority"], r["label"]))
227
+ untracked = find_untracked_dirs(project_root, tracked_modules)
228
+ return {"rows": rows, "untracked": untracked}
229
+
230
+
231
+ # ─── Formatters ───────────────────────────────────────────────────────────────
232
+
233
+ def format_markdown(data: dict, generated_at: str) -> str:
234
+ rows = data["rows"]
235
+ untracked = data["untracked"]
236
+
237
+ lines = [
238
+ f"<!-- generated: {generated_at} -->",
239
+ "# Knowledge Base Coverage Matrix",
240
+ "",
241
+ f"> Generated by: `scripts/audit_coverage.py` ",
242
+ f"> Date: {generated_at}",
243
+ "",
244
+ "## Module Coverage",
245
+ "",
246
+ "| Module | Label | KI | Cov | KB | Density | Priority | Status |",
247
+ "|---|---|:---:|:---:|---:|---:|---|---|",
248
+ ]
249
+
250
+ for r in rows:
251
+ ki = "✅" if r["has_ki"] else "❌"
252
+ cov = f"{r['coverage_pct']}%"
253
+ p = priority_label(r["importance"])
254
+ warn = ""
255
+ if r["complex_kis"]:
256
+ warn += " 🔥" # Back to fire icon
257
+ if r["has_ki"] and r["density"] < DENSITY_THRESHOLD and r["size_kb"] > 5:
258
+ warn += " ❄️"
259
+ lines.append(
260
+ f"| `{r['module']}` | {r['label']}{warn} | {ki} | {cov} | "
261
+ f"{r['size_kb']} | {r['density']} | {p} | {r['status']} |"
262
+ )
263
+
264
+ total_kb = sum(r["size_kb"] for r in rows)
265
+ covered_kb = sum(r["covered_size_kb"] for r in rows)
266
+ progress = round(covered_kb / total_kb * 100) if total_kb > 0 else 0
267
+ fully_covered_count = sum(1 for r in rows if r["coverage_pct"] == 100)
268
+ total_count = len(rows)
269
+
270
+ lines += [
271
+ "",
272
+ "## Summary",
273
+ "",
274
+ "| Metric | Value |",
275
+ "|---|---|",
276
+ f"| ✅ Fully Covered Modules | {fully_covered_count} / {total_count} |",
277
+ f"| 📚 Total Code Size | {round(total_kb, 1)} KB |",
278
+ f"| 📖 Documented Code | {round(covered_kb, 1)} KB |",
279
+ f"| 📊 Full Progress | **{progress}%** |",
280
+ "",
281
+ ]
282
+
283
+ complex_list = [r for r in rows if r["complex_kis"]]
284
+ if complex_list:
285
+ lines += [
286
+ "## 🚀 Refactoring Opportunities (High Complexity)",
287
+ "",
288
+ "The following Knowledge Items are covering too many files (>10). ",
289
+ "This is a **key area for improvement**: splitting these KIs into smaller, ",
290
+ "more focused ones will improve documentation maintainability and search precision.",
291
+ "",
292
+ ]
293
+ for r in complex_list:
294
+ kis = ", ".join(f"🔥 `{k}`" for k in r["complex_kis"])
295
+ lines.append(f"- **{r['label']}**: {kis} — *Consider splitting by sub-modules*")
296
+ lines.append("")
297
+
298
+ if untracked:
299
+ lines += [
300
+ "## ⚠️ Undocumented Areas (Blind Spots)",
301
+ "",
302
+ "The following directories contain code but are not included in the audit:",
303
+ "",
304
+ ]
305
+ for u in untracked:
306
+ lines.append(f"- `{u}`")
307
+ lines.append("")
308
+
309
+ lines += ["## Recommendations for /expand-knowledge", ""]
310
+ top = [r for r in rows
311
+ if r["coverage_pct"] < 100
312
+ or (r["has_ki"] and r["density"] < DENSITY_THRESHOLD and r["size_kb"] > 5)][:5]
313
+ for i, r in enumerate(top, 1):
314
+ missing = []
315
+ if not r["has_ki"]:
316
+ missing.append("no KI file")
317
+ elif r["density"] < DENSITY_THRESHOLD:
318
+ missing.append(f"low knowledge density ({r['density']} bytes/KB)")
319
+ if r["coverage_pct"] < 100:
320
+ missing.append(f"only {r['coverage_pct']}% files covered")
321
+ lines += [
322
+ f"### {i}. {r['label']} — {priority_label(r['importance'])}",
323
+ f"- **Path**: `{r['module']}`",
324
+ f"- **Size**: {r['size_kb']} KB, {r['files']} files",
325
+ f"- **Gaps**: {', '.join(missing)}",
326
+ "",
327
+ ]
328
+
329
+ return "\n".join(lines)
330
+
331
+
332
+ # ─── Entry Point ──────────────────────────────────────────────────────────────
333
+
334
+ def main():
335
+ parser = argparse.ArgumentParser(description="Knowledge base coverage audit.")
336
+ parser.add_argument("--root", default=".", help="Project root directory (default: current)")
337
+ parser.add_argument("--no-save", action="store_true", help="Do not save coverage_matrix.md")
338
+ parser.add_argument("--output", default=None, help="Custom output path for coverage_matrix.md")
339
+ args = parser.parse_args()
340
+
341
+ project_root = get_project_root()
342
+ tracked_modules = load_tracked_modules()
343
+ if not tracked_modules:
344
+ print("[!] No tracked_modules found in doc_config.json. Please fill in coverage_settings.")
345
+ sys.exit(0)
346
+
347
+ print(f"[*] Auditing project: {project_root}")
348
+ data = build_coverage_matrix(project_root, tracked_modules)
349
+
350
+ # Terminal output
351
+ print("\n" + "=" * 70)
352
+ print(" COVERAGE MATRIX")
353
+ print("=" * 70)
354
+ header = f"{'Module':<32} {'KI':^3} {'Cov':>6} | {'Density':>8}"
355
+ print(header)
356
+ print("-" * 70)
357
+ for r in data["rows"]:
358
+ ki = "✅" if r["has_ki"] else "❌"
359
+ warn = ""
360
+ if r["complex_kis"]:
361
+ warn += " 🔥"
362
+ if r["has_ki"] and r["density"] < DENSITY_THRESHOLD and r["size_kb"] > 5:
363
+ warn += " ❄️"
364
+
365
+ module_display = f"{r['label']}{warn}"
366
+ print(f"{module_display:<32} {ki:^3} {r['coverage_pct']:>5}% | {r['density']:>6.1f} B/KB")
367
+
368
+ # Detailed Warnings
369
+ complex_list = [r for r in data["rows"] if r["complex_kis"]]
370
+ if complex_list:
371
+ print("\n🔥 COMPLEXITY WARNING (Consider splitting):")
372
+ for r in complex_list:
373
+ kis = ", ".join(r["complex_kis"])
374
+ print(f" - {r['label']}: {kis}")
375
+
376
+ low_density = [r for r in data["rows"] if r["has_ki"] and r["density"] < DENSITY_THRESHOLD and r["size_kb"] > 5]
377
+ if low_density:
378
+ print("\n❄️ LOW DENSITY (Documentation too brief):")
379
+ for r in low_density:
380
+ print(f" - {r['label']} ({r['density']:.1f} B/KB)")
381
+
382
+ if data["untracked"]:
383
+ print("\n⚠️ UNTRACKED AREAS (Blind Spots):")
384
+ for u in data["untracked"]:
385
+ print(f" - {u}")
386
+
387
+ # Save Markdown
388
+ if not args.no_save:
389
+ generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d")
390
+ md_content = format_markdown(data, generated_at)
391
+ output_path = args.output or os.path.join(get_knowledge_root(), "coverage_matrix.md")
392
+ output_path = os.path.abspath(output_path)
393
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
394
+ with open(output_path, "w", encoding="utf-8") as f:
395
+ f.write(md_content)
396
+ print(f"[+] Matrix saved: {output_path}")
397
+
398
+
399
+ EXCLUDED_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv", "dist", "build"}
400
+ DENSITY_THRESHOLD = 50.0 # KI bytes per 1 KB of code
401
+ COMPLEXITY_THRESHOLD = 10 # Files per one KI
402
+
403
+ if __name__ == "__main__":
404
+ if sys.platform == "win32":
405
+ sys.stdout.reconfigure(encoding="utf-8")
406
+ main()
@@ -0,0 +1,81 @@
1
+ import os
2
+ import sys
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ # Добавляем путь к ki_utils
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+ import ki_utils
10
+
11
+ def find_unmapped_files(target_path: str):
12
+ project_root = ki_utils.get_project_root()
13
+ doc_config = ki_utils.get_doc_config()
14
+
15
+ if not project_root or not doc_config:
16
+ print("Error: Could not resolve project root or doc_config.json")
17
+ return
18
+
19
+ # 1. Собираем все файлы, которые уже замаплены в doc_config.json
20
+ mapped_paths = set()
21
+ knowledge_items = doc_config.get("knowledge_items", {})
22
+
23
+ for ki in knowledge_items.values():
24
+ depends_on = ki.get("depends_on", [])
25
+ for p in depends_on:
26
+ # Превращаем в абсолютный путь для корректного сравнения
27
+ abs_p = os.path.abspath(os.path.join(project_root, p))
28
+ mapped_paths.add(abs_p)
29
+
30
+ # 2. Сканируем целевую директорию
31
+ abs_target = os.path.abspath(os.path.join(project_root, target_path))
32
+ if not os.path.exists(abs_target):
33
+ print(f"Error: Target path '{target_path}' does not exist.")
34
+ return
35
+
36
+ unmapped = []
37
+
38
+ # Расширения, которые мы обычно игнорируем (артефакты компиляции и т.д.)
39
+ ignored_exts = {'.pyc', '.pyo', '.pyd', '.obj', '.dll', '.exe', '.bin'}
40
+ ignored_dirs = {'__pycache__', '.git', '.venv', 'node_modules'}
41
+
42
+ for root, dirs, files in os.walk(abs_target):
43
+ # Фильтруем игнорируемые директории
44
+ dirs[:] = [d for d in dirs if d not in ignored_dirs]
45
+
46
+ for f in files:
47
+ if any(f.endswith(ext) for ext in ignored_exts):
48
+ continue
49
+
50
+ full_path = os.path.abspath(os.path.join(root, f))
51
+
52
+ # Проверяем, замаплен ли файл напрямую
53
+ if full_path in mapped_paths:
54
+ continue
55
+
56
+ # Проверяем, не замаплена ли родительская директория (как папка)
57
+ is_parent_mapped = False
58
+ for mapped in mapped_paths:
59
+ if os.path.isdir(mapped):
60
+ if full_path.startswith(os.path.join(mapped, "")):
61
+ is_parent_mapped = True
62
+ break
63
+
64
+ if not is_parent_mapped:
65
+ # Возвращаем путь относительно корня проекта для удобства
66
+ rel_to_project = os.path.relpath(full_path, project_root)
67
+ unmapped.append(rel_to_project)
68
+
69
+ if unmapped:
70
+ print(f"### Unmapped Files in '{target_path}':")
71
+ for f in sorted(unmapped):
72
+ print(f"- {f}")
73
+ else:
74
+ print(f"All files in '{target_path}' are already mapped in doc_config.json.")
75
+
76
+ if __name__ == "__main__":
77
+ parser = argparse.ArgumentParser(description="Find files in a directory not covered by any KI in doc_config.json")
78
+ parser.add_argument("path", help="Relative path from project root to scan")
79
+ args = parser.parse_args()
80
+
81
+ find_unmapped_files(args.path)