ki-manager 2.3.0.dev1__py3-none-any.whl → 2.3.0.dev3__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 +1 -1
- ki_manager/cli.py +3 -3
- ki_manager/scripts/analyze_module.py +4 -4
- ki_manager/scripts/check_ki_drift.py +382 -0
- ki_manager/scripts/finalize_ki_scaffolds.py +2 -2
- ki_manager/scripts/find_unmapped_files.py +3 -3
- ki_manager/scripts/generate_ki_scaffolds.py +282 -31
- ki_manager/scripts/ki_search.py +41 -4
- ki_manager/scripts/ki_utils.py +14 -1
- ki_manager/server.py +387 -32
- ki_manager/templates/KI_template.md +27 -0
- ki_manager/tools/facade.py +22 -5
- ki_manager/tools/scaffold.py +7 -3
- ki_manager/workflows/create-adr.md +7 -5
- ki_manager/workflows/expand-knowledge.md +10 -0
- ki_manager/workflows/scaffold-knowledge.md +7 -0
- ki_manager/workflows/sync-knowledge.md +18 -9
- {ki_manager-2.3.0.dev1.dist-info → ki_manager-2.3.0.dev3.dist-info}/METADATA +5 -2
- ki_manager-2.3.0.dev3.dist-info/RECORD +30 -0
- ki_manager-2.3.0.dev1.dist-info/RECORD +0 -28
- {ki_manager-2.3.0.dev1.dist-info → ki_manager-2.3.0.dev3.dist-info}/WHEEL +0 -0
- {ki_manager-2.3.0.dev1.dist-info → ki_manager-2.3.0.dev3.dist-info}/entry_points.txt +0 -0
- {ki_manager-2.3.0.dev1.dist-info → ki_manager-2.3.0.dev3.dist-info}/licenses/LICENSE +0 -0
ki_manager/__init__.py
CHANGED
ki_manager/cli.py
CHANGED
|
@@ -20,10 +20,10 @@ _WORKFLOWS_DIR = _PACKAGE_DIR / "workflows"
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
|
23
|
-
"""
|
|
23
|
+
"""Parses YAML frontmatter from a markdown file.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
Returns (meta_dict, body_text).
|
|
26
|
+
If there is no frontmatter, returns ({}, text).
|
|
27
27
|
"""
|
|
28
28
|
if not text.startswith("---"):
|
|
29
29
|
return {}, text
|
|
@@ -4,7 +4,7 @@ import argparse
|
|
|
4
4
|
import io
|
|
5
5
|
from typing import Dict, List, Optional
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# Add path to scripts to import ki_utils
|
|
8
8
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
9
9
|
import ki_utils
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ def format_size(size_bytes: int) -> str:
|
|
|
17
17
|
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
|
18
18
|
|
|
19
19
|
def get_tracked_files(config: Dict) -> Dict[str, str]:
|
|
20
|
-
"""
|
|
20
|
+
"""Returns a map {file_path: KI_name}"""
|
|
21
21
|
tracked = {}
|
|
22
22
|
ki_items = config.get("knowledge_items", {})
|
|
23
23
|
for ki_name, ki_data in ki_items.items():
|
|
@@ -30,7 +30,7 @@ def analyze_path(target_path: str, recursive: bool = False):
|
|
|
30
30
|
config = ki_utils.get_doc_config()
|
|
31
31
|
tracked_map = get_tracked_files(config)
|
|
32
32
|
|
|
33
|
-
#
|
|
33
|
+
# Resolve absolute path
|
|
34
34
|
abs_target = os.path.normpath(os.path.join(project_root, target_path))
|
|
35
35
|
|
|
36
36
|
if not os.path.exists(abs_target):
|
|
@@ -87,7 +87,7 @@ def analyze_path(target_path: str, recursive: bool = False):
|
|
|
87
87
|
continue
|
|
88
88
|
|
|
89
89
|
def main():
|
|
90
|
-
#
|
|
90
|
+
# Fix encoding for Windows when run directly
|
|
91
91
|
if sys.platform == "win32":
|
|
92
92
|
import io
|
|
93
93
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""
|
|
2
|
+
check_ki_drift.py
|
|
3
|
+
|
|
4
|
+
Detects discrepancies (drift) between Knowledge Items (KI) and the actual codebase:
|
|
5
|
+
1. Symbol Drift:
|
|
6
|
+
- Dead symbols in Key Components table (deleted/renamed in code).
|
|
7
|
+
- Unmapped public symbols in source files (new code not yet in KI).
|
|
8
|
+
2. File Drift:
|
|
9
|
+
- Broken or missing file paths in doc_config.json depends_on.
|
|
10
|
+
3. Date Drift:
|
|
11
|
+
- Source files modified in git after KI's <!-- last_verified: YYYY-MM-DD -->.
|
|
12
|
+
4. Broken Wiki-links:
|
|
13
|
+
- [[Target_KI.md]] links in Related KIs that do not exist.
|
|
14
|
+
5. Integrity Drift:
|
|
15
|
+
- Pending scaffolds (<!-- scaffold: true -->) or remaining <!-- TODO --> markers.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python check_ki_drift.py --all
|
|
19
|
+
python check_ki_drift.py --ki KI_server.md
|
|
20
|
+
python check_ki_drift.py --json
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import re
|
|
26
|
+
import json
|
|
27
|
+
import argparse
|
|
28
|
+
import subprocess
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Dict, List, Tuple, Optional, Any, Set
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
33
|
+
import ki_utils
|
|
34
|
+
from generate_ki_scaffolds import extract_symbol_details
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_ki_file(ki_path: Path) -> Dict[str, Any]:
|
|
38
|
+
"""Parse a KI markdown file and extract its metadata, tables, and links."""
|
|
39
|
+
try:
|
|
40
|
+
content = ki_path.read_text(encoding="utf-8", errors="replace")
|
|
41
|
+
except OSError:
|
|
42
|
+
return {
|
|
43
|
+
"error": f"Cannot read {ki_path.name}",
|
|
44
|
+
"last_verified": None,
|
|
45
|
+
"is_scaffold": False,
|
|
46
|
+
"is_enriched": False,
|
|
47
|
+
"has_todo": False,
|
|
48
|
+
"table_symbols": [],
|
|
49
|
+
"wiki_links": [],
|
|
50
|
+
"sections": set(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines = content.splitlines()
|
|
54
|
+
last_verified = None
|
|
55
|
+
is_scaffold = False
|
|
56
|
+
is_enriched = False
|
|
57
|
+
has_todo = "<!-- TODO" in content or "TODO:" in content
|
|
58
|
+
|
|
59
|
+
# Check top lines for markers
|
|
60
|
+
for line in lines[:5]:
|
|
61
|
+
line_clean = line.strip()
|
|
62
|
+
if "<!-- scaffold: true -->" in line_clean:
|
|
63
|
+
is_scaffold = True
|
|
64
|
+
elif "<!-- scaffold: enriched -->" in line_clean:
|
|
65
|
+
is_enriched = True
|
|
66
|
+
elif line_clean.startswith("<!-- last_verified:"):
|
|
67
|
+
parts = line_clean.split(":", 1)
|
|
68
|
+
if len(parts) > 1:
|
|
69
|
+
last_verified = parts[1].replace("-->", "").strip()
|
|
70
|
+
|
|
71
|
+
# Extract sections
|
|
72
|
+
sections = set()
|
|
73
|
+
for line in lines:
|
|
74
|
+
if line.startswith("## "):
|
|
75
|
+
sections.add(line[3:].strip())
|
|
76
|
+
|
|
77
|
+
# Extract Key Components table symbols
|
|
78
|
+
# Expected table format: | `SymbolName` | `rel/path/file.ext` | Purpose |
|
|
79
|
+
table_symbols: List[Dict[str, str]] = []
|
|
80
|
+
in_key_components = False
|
|
81
|
+
table_row_pattern = re.compile(r"\|\s*`?([A-Za-z0-9_]+)`?\s*\|\s*`?([^`|\s]+)`?\s*\|")
|
|
82
|
+
|
|
83
|
+
for line in lines:
|
|
84
|
+
if line.startswith("## Key Components"):
|
|
85
|
+
in_key_components = True
|
|
86
|
+
continue
|
|
87
|
+
elif in_key_components and line.startswith("## "):
|
|
88
|
+
in_key_components = False
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
if in_key_components and line.strip().startswith("|"):
|
|
92
|
+
lower = line.lower()
|
|
93
|
+
if any(k in lower for k in ["class / function", "component", "|---", "|:---", "| — |", "| - |"]):
|
|
94
|
+
continue
|
|
95
|
+
m = table_row_pattern.search(line)
|
|
96
|
+
if m:
|
|
97
|
+
sym_name = m.group(1).strip()
|
|
98
|
+
file_rel = m.group(2).strip()
|
|
99
|
+
header_keywords = {"component", "file", "purpose", "class", "function", "description"}
|
|
100
|
+
if sym_name.lower() in header_keywords or file_rel.lower() in header_keywords:
|
|
101
|
+
continue
|
|
102
|
+
if sym_name and file_rel and not file_rel.startswith("—"):
|
|
103
|
+
table_symbols.append({"symbol": sym_name, "file": file_rel})
|
|
104
|
+
|
|
105
|
+
# Extract wiki-links [[KI_*.md]]
|
|
106
|
+
wiki_links = []
|
|
107
|
+
for link_match in re.finditer(r"\[\[([^\]]+)\]\]", content):
|
|
108
|
+
wiki_links.append(link_match.group(1).strip())
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"last_verified": last_verified,
|
|
112
|
+
"is_scaffold": is_scaffold,
|
|
113
|
+
"is_enriched": is_enriched,
|
|
114
|
+
"has_todo": has_todo,
|
|
115
|
+
"table_symbols": table_symbols,
|
|
116
|
+
"wiki_links": wiki_links,
|
|
117
|
+
"sections": sections,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_git_commit_date(project_root: str, rel_path: str) -> Optional[str]:
|
|
122
|
+
"""Returns the ISO date (YYYY-MM-DD) of the last git commit touching rel_path."""
|
|
123
|
+
try:
|
|
124
|
+
res = subprocess.run(
|
|
125
|
+
["git", "log", "-1", "--format=%cs", "--", rel_path],
|
|
126
|
+
cwd=project_root,
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
encoding="utf-8",
|
|
130
|
+
timeout=5,
|
|
131
|
+
)
|
|
132
|
+
if res.returncode == 0:
|
|
133
|
+
date_str = res.stdout.strip()
|
|
134
|
+
return date_str if date_str else None
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def check_single_ki(
|
|
141
|
+
ki_name: str,
|
|
142
|
+
ki_path: Path,
|
|
143
|
+
doc_config: dict,
|
|
144
|
+
project_root: str,
|
|
145
|
+
knowledge_dir: Path
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""Inspects a single KI file against code and configuration for drift."""
|
|
148
|
+
ki_data = parse_ki_file(ki_path)
|
|
149
|
+
if "error" in ki_data:
|
|
150
|
+
return {"ki": ki_name, "error": ki_data["error"], "drift_count": 1}
|
|
151
|
+
|
|
152
|
+
config_entry = doc_config.get("knowledge_items", {}).get(ki_name, {})
|
|
153
|
+
depends_on = config_entry.get("depends_on", [])
|
|
154
|
+
|
|
155
|
+
dead_symbols: List[Dict[str, str]] = []
|
|
156
|
+
unmapped_symbols: List[Dict[str, Any]] = []
|
|
157
|
+
missing_files: List[str] = []
|
|
158
|
+
date_drifts: List[Dict[str, str]] = []
|
|
159
|
+
broken_links: List[str] = []
|
|
160
|
+
integrity_issues: List[str] = []
|
|
161
|
+
|
|
162
|
+
# 1. Integrity issues
|
|
163
|
+
if ki_data["is_scaffold"]:
|
|
164
|
+
integrity_issues.append("Scaffold stub is still pending (<!-- scaffold: true -->)")
|
|
165
|
+
elif ki_data["is_enriched"]:
|
|
166
|
+
integrity_issues.append("Scaffold is enriched but not finalized (run ki_finalize_scaffolds)")
|
|
167
|
+
if ki_data["has_todo"]:
|
|
168
|
+
integrity_issues.append("Contains unpopulated <!-- TODO --> markers")
|
|
169
|
+
|
|
170
|
+
# 2. Check depends_on files existence
|
|
171
|
+
for dep in depends_on:
|
|
172
|
+
dep_path = Path(project_root) / dep
|
|
173
|
+
if not dep_path.exists():
|
|
174
|
+
missing_files.append(dep)
|
|
175
|
+
|
|
176
|
+
# 3. Check wiki-links
|
|
177
|
+
for link in ki_data["wiki_links"]:
|
|
178
|
+
# Standardize target name
|
|
179
|
+
target_name = link if link.endswith(".md") else f"{link}.md"
|
|
180
|
+
target_path = knowledge_dir / target_name
|
|
181
|
+
if not target_path.exists():
|
|
182
|
+
broken_links.append(link)
|
|
183
|
+
|
|
184
|
+
# 4. Group table symbols by file to check symbol drift
|
|
185
|
+
symbols_by_file: Dict[str, Set[str]] = {}
|
|
186
|
+
for entry in ki_data["table_symbols"]:
|
|
187
|
+
f_norm = entry["file"].replace("\\", "/")
|
|
188
|
+
symbols_by_file.setdefault(f_norm, set()).add(entry["symbol"])
|
|
189
|
+
|
|
190
|
+
# Determine files to inspect for symbol drift
|
|
191
|
+
files_to_inspect = set(symbols_by_file.keys())
|
|
192
|
+
for dep in depends_on:
|
|
193
|
+
dep_norm = dep.replace("\\", "/")
|
|
194
|
+
dep_abs = Path(project_root) / dep
|
|
195
|
+
if dep_abs.is_file() and dep_abs.suffix.lower() in (".py", ".ts", ".tsx", ".js", ".jsx", ".go"):
|
|
196
|
+
files_to_inspect.add(dep_norm)
|
|
197
|
+
|
|
198
|
+
for rel_file in files_to_inspect:
|
|
199
|
+
abs_file = Path(project_root) / rel_file
|
|
200
|
+
if not abs_file.exists():
|
|
201
|
+
if rel_file not in missing_files:
|
|
202
|
+
missing_files.append(rel_file)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Extract real symbols from code
|
|
206
|
+
code_details = extract_symbol_details(str(abs_file))
|
|
207
|
+
code_symbols_dict = {s["name"]: s for s in code_details.get("symbols", [])}
|
|
208
|
+
code_symbols_set = set(code_symbols_dict.keys())
|
|
209
|
+
|
|
210
|
+
# Check dead symbols (present in KI table, absent in code)
|
|
211
|
+
documented_symbols = symbols_by_file.get(rel_file, set())
|
|
212
|
+
for doc_sym in documented_symbols:
|
|
213
|
+
if doc_sym not in code_symbols_set:
|
|
214
|
+
dead_symbols.append({"symbol": doc_sym, "file": rel_file})
|
|
215
|
+
|
|
216
|
+
# Check unmapped public symbols in source files
|
|
217
|
+
# (Only report top-level non-private symbols)
|
|
218
|
+
for sym_name, sym_info in code_symbols_dict.items():
|
|
219
|
+
if sym_name not in documented_symbols and not sym_name.startswith("_"):
|
|
220
|
+
unmapped_symbols.append({
|
|
221
|
+
"symbol": sym_name,
|
|
222
|
+
"kind": sym_info.get("kind", "symbol"),
|
|
223
|
+
"file": rel_file,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
# Check date drift if last_verified is present
|
|
227
|
+
if ki_data["last_verified"]:
|
|
228
|
+
last_commit_date = get_git_commit_date(project_root, rel_file)
|
|
229
|
+
if last_commit_date and last_commit_date > ki_data["last_verified"]:
|
|
230
|
+
date_drifts.append({
|
|
231
|
+
"file": rel_file,
|
|
232
|
+
"commit_date": last_commit_date,
|
|
233
|
+
"last_verified": ki_data["last_verified"],
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
drift_count = (
|
|
237
|
+
len(dead_symbols)
|
|
238
|
+
+ len(unmapped_symbols)
|
|
239
|
+
+ len(missing_files)
|
|
240
|
+
+ len(date_drifts)
|
|
241
|
+
+ len(broken_links)
|
|
242
|
+
+ len(integrity_issues)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"ki": ki_name,
|
|
247
|
+
"drift_count": drift_count,
|
|
248
|
+
"last_verified": ki_data["last_verified"],
|
|
249
|
+
"dead_symbols": dead_symbols,
|
|
250
|
+
"unmapped_symbols": unmapped_symbols,
|
|
251
|
+
"missing_files": missing_files,
|
|
252
|
+
"date_drifts": date_drifts,
|
|
253
|
+
"broken_links": broken_links,
|
|
254
|
+
"integrity_issues": integrity_issues,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_drift_check(
|
|
259
|
+
ki_filter: Optional[str] = None,
|
|
260
|
+
project_root: Optional[str] = None,
|
|
261
|
+
knowledge_root: Optional[str] = None,
|
|
262
|
+
) -> List[Dict[str, Any]]:
|
|
263
|
+
"""Runs drift check on one or all KIs."""
|
|
264
|
+
if not project_root:
|
|
265
|
+
project_root = ki_utils.get_project_root()
|
|
266
|
+
if not knowledge_root:
|
|
267
|
+
knowledge_root = ki_utils.get_knowledge_root()
|
|
268
|
+
|
|
269
|
+
if not knowledge_root or not project_root:
|
|
270
|
+
raise RuntimeError("Could not determine project root or knowledge root.")
|
|
271
|
+
|
|
272
|
+
knowledge_dir = Path(knowledge_root) / "knowledge"
|
|
273
|
+
if not knowledge_dir.exists() and Path(knowledge_root).exists():
|
|
274
|
+
knowledge_dir = Path(knowledge_root)
|
|
275
|
+
|
|
276
|
+
if not knowledge_dir.exists():
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
doc_config = ki_utils.get_doc_config()
|
|
280
|
+
|
|
281
|
+
if ki_filter:
|
|
282
|
+
target_name = ki_filter if ki_filter.endswith(".md") else f"{ki_filter}.md"
|
|
283
|
+
target_path = knowledge_dir / target_name
|
|
284
|
+
if not target_path.exists():
|
|
285
|
+
return [{"ki": target_name, "error": f"File not found: {target_name}", "drift_count": 1}]
|
|
286
|
+
return [check_single_ki(target_name, target_path, doc_config, project_root, knowledge_dir)]
|
|
287
|
+
|
|
288
|
+
results = []
|
|
289
|
+
for f in sorted(knowledge_dir.glob("KI_*.md")):
|
|
290
|
+
results.append(check_single_ki(f.name, f, doc_config, project_root, knowledge_dir))
|
|
291
|
+
|
|
292
|
+
return results
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def format_drift_markdown(results: List[Dict[str, Any]]) -> str:
|
|
296
|
+
"""Formats drift check results as clean Markdown."""
|
|
297
|
+
total_kis = len(results)
|
|
298
|
+
with_drift = [r for r in results if r.get("drift_count", 0) > 0]
|
|
299
|
+
|
|
300
|
+
lines = [
|
|
301
|
+
f"### Knowledge Base Drift Report",
|
|
302
|
+
f"- **Checked KIs**: {total_kis}",
|
|
303
|
+
f"- **KIs with drift**: {len(with_drift)}",
|
|
304
|
+
"",
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
if not with_drift:
|
|
308
|
+
lines.append("✅ **No drift detected.** All Knowledge Items match current code and git state.")
|
|
309
|
+
return "\n".join(lines)
|
|
310
|
+
|
|
311
|
+
for item in with_drift:
|
|
312
|
+
ki_name = item["ki"]
|
|
313
|
+
lines.append(f"#### ⚠️ `{ki_name}` ({item['drift_count']} issues)")
|
|
314
|
+
|
|
315
|
+
if item.get("error"):
|
|
316
|
+
lines.append(f"- [!] Error: {item['error']}")
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
for issue in item.get("integrity_issues", []):
|
|
320
|
+
lines.append(f"- [!] **Integrity**: {issue}")
|
|
321
|
+
|
|
322
|
+
for dead in item.get("dead_symbols", []):
|
|
323
|
+
lines.append(f"- [!] **Dead symbol**: `{dead['symbol']}` (no longer in `{dead['file']}`)")
|
|
324
|
+
|
|
325
|
+
for missing in item.get("missing_files", []):
|
|
326
|
+
lines.append(f"- [!] **Missing file**: `{missing}` (does not exist on disk)")
|
|
327
|
+
|
|
328
|
+
for broken in item.get("broken_links", []):
|
|
329
|
+
lines.append(f"- [!] **Broken wiki-link**: `[[{broken}]]` (target KI does not exist)")
|
|
330
|
+
|
|
331
|
+
for date_item in item.get("date_drifts", []):
|
|
332
|
+
lines.append(
|
|
333
|
+
f"- [~] **Date drift**: `{date_item['file']}` was committed on "
|
|
334
|
+
f"`{date_item['commit_date']}`, after last_verified (`{date_item['last_verified']}`)"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
unmapped = item.get("unmapped_symbols", [])
|
|
338
|
+
if unmapped:
|
|
339
|
+
sample = unmapped[:5]
|
|
340
|
+
more = len(unmapped) - 5
|
|
341
|
+
unmapped_str = ", ".join(f"`{u['symbol']}`" for u in sample)
|
|
342
|
+
if more > 0:
|
|
343
|
+
unmapped_str += f" and {more} more"
|
|
344
|
+
lines.append(f"- [+] **Unmapped symbols**: {unmapped_str} in `{unmapped[0]['file']}`")
|
|
345
|
+
|
|
346
|
+
lines.append("")
|
|
347
|
+
|
|
348
|
+
return "\n".join(lines).strip()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def main():
|
|
352
|
+
if sys.platform == "win32":
|
|
353
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
354
|
+
|
|
355
|
+
parser = argparse.ArgumentParser(description="Check drift and discrepancies between KI files and codebase.")
|
|
356
|
+
parser.add_argument("--ki", type=str, help="Specific KI file to check (e.g. KI_server.md)")
|
|
357
|
+
parser.add_argument("--all", action="store_true", help="Check all KIs in the knowledge base")
|
|
358
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
359
|
+
parser.add_argument("--workspace", type=str, default=None, help=argparse.SUPPRESS)
|
|
360
|
+
|
|
361
|
+
args = parser.parse_args()
|
|
362
|
+
|
|
363
|
+
if args.workspace:
|
|
364
|
+
ki_utils.ACTIVE_WORKSPACE_PATH = ki_utils.normalize_path(args.workspace)
|
|
365
|
+
else:
|
|
366
|
+
match = ki_utils.find_project_by_cwd(os.getcwd())
|
|
367
|
+
if match:
|
|
368
|
+
target_path = match.get("config_path") or match.get("workspace") or os.getcwd()
|
|
369
|
+
ki_utils.ACTIVE_WORKSPACE_PATH = ki_utils.normalize_path(target_path)
|
|
370
|
+
else:
|
|
371
|
+
ki_utils.ACTIVE_WORKSPACE_PATH = ki_utils.normalize_path(os.getcwd())
|
|
372
|
+
|
|
373
|
+
results = run_drift_check(ki_filter=args.ki)
|
|
374
|
+
|
|
375
|
+
if args.json:
|
|
376
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
377
|
+
else:
|
|
378
|
+
print(format_drift_markdown(results))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if __name__ == "__main__":
|
|
382
|
+
main()
|
|
@@ -29,8 +29,8 @@ PENDING_MARKER = "<!-- scaffold: true -->"
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def extract_overview(content: str) -> str:
|
|
32
|
-
"""Extract the first non-empty paragraph from the Overview section."""
|
|
33
|
-
match = re.search(r"## Overview\s*\n(.*?)(?=\n##|\Z)", content, re.DOTALL)
|
|
32
|
+
"""Extract the first non-empty paragraph from the Overview section (or legacy Purpose / What it Does)."""
|
|
33
|
+
match = re.search(r"## (?:Overview|Purpose|What it Does)\s*\n(.*?)(?=\n##|\Z)", content, re.DOTALL)
|
|
34
34
|
if not match:
|
|
35
35
|
return ""
|
|
36
36
|
text = match.group(1).strip()
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import List
|
|
7
7
|
|
|
8
|
-
#
|
|
8
|
+
# Add path to ki_utils
|
|
9
9
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
10
10
|
import ki_utils
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ def get_unmapped_files(target_path: str = ".") -> List[str]:
|
|
|
17
17
|
if not project_root or not doc_config:
|
|
18
18
|
return []
|
|
19
19
|
|
|
20
|
-
# 1.
|
|
20
|
+
# 1. Collect all files that are already mapped in doc_config.json
|
|
21
21
|
mapped_paths = set()
|
|
22
22
|
knowledge_items = doc_config.get("knowledge_items", {})
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ def get_unmapped_files(target_path: str = ".") -> List[str]:
|
|
|
27
27
|
abs_p = os.path.abspath(os.path.join(project_root, p))
|
|
28
28
|
mapped_paths.add(abs_p)
|
|
29
29
|
|
|
30
|
-
# 2.
|
|
30
|
+
# 2. Scan the target directory
|
|
31
31
|
abs_target = os.path.abspath(os.path.join(project_root, target_path))
|
|
32
32
|
if not os.path.exists(abs_target):
|
|
33
33
|
return []
|