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 +2 -0
- ki_manager/scripts/add_ki_to_config.py +57 -0
- ki_manager/scripts/analyze_module.py +103 -0
- ki_manager/scripts/audit_coverage.py +406 -0
- ki_manager/scripts/find_unmapped_files.py +81 -0
- ki_manager/scripts/generate_dir_index.py +140 -0
- ki_manager/scripts/ki_dependency_analyzer.py +233 -0
- ki_manager/scripts/ki_utils.py +279 -0
- ki_manager/scripts/knowledge_engine.py +232 -0
- ki_manager/scripts/sync_agents_md.py +77 -0
- ki_manager/server.py +632 -0
- ki_manager/tools/__init__.py +1 -0
- ki_manager/tools/scaffold.py +272 -0
- ki_manager-2.0.0.dist-info/METADATA +193 -0
- ki_manager-2.0.0.dist-info/RECORD +18 -0
- ki_manager-2.0.0.dist-info/WHEEL +4 -0
- ki_manager-2.0.0.dist-info/entry_points.txt +2 -0
- ki_manager-2.0.0.dist-info/licenses/LICENSE +21 -0
ki_manager/__init__.py
ADDED
|
@@ -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)
|