ki-manager 2.3.0.dev2__tar.gz → 2.3.1.dev1__tar.gz

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.
Files changed (32) hide show
  1. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/.gitignore +3 -0
  2. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/CHANGELOG.md +45 -0
  3. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/PKG-INFO +5 -2
  4. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/README.md +4 -1
  5. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/pyproject.toml +1 -1
  6. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/__init__.py +1 -1
  7. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/cli.py +3 -3
  8. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/analyze_module.py +7 -4
  9. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/audit_coverage.py +1 -1
  10. ki_manager-2.3.1.dev1/src/ki_manager/scripts/check_ki_drift.py +382 -0
  11. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/finalize_ki_scaffolds.py +3 -3
  12. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/find_unmapped_files.py +5 -3
  13. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/generate_ki_scaffolds.py +283 -32
  14. ki_manager-2.3.1.dev1/src/ki_manager/scripts/git_diff_secured.py +81 -0
  15. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_search.py +2 -2
  16. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_utils.py +58 -3
  17. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/knowledge_engine.py +5 -4
  18. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/sync_agents_md.py +8 -4
  19. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/server.py +609 -157
  20. ki_manager-2.3.1.dev1/src/ki_manager/templates/KI_template.md +27 -0
  21. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/tools/facade.py +47 -2
  22. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/tools/scaffold.py +14 -9
  23. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/create-adr.md +7 -5
  24. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/expand-knowledge.md +14 -4
  25. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/scaffold-knowledge.md +7 -0
  26. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/sync-knowledge.md +20 -11
  27. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/LICENSE +0 -0
  28. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/add_ki_to_config.py +0 -0
  29. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/generate_dir_index.py +0 -0
  30. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_dependency_analyzer.py +0 -0
  31. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_graph_visualize.py +0 -0
  32. {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/tools/__init__.py +0 -0
@@ -29,3 +29,6 @@ posts/
29
29
 
30
30
  # Private plans (moved to KI-enterprise)
31
31
  plans/
32
+
33
+ # Agent/MCP state
34
+ .serena/
@@ -9,6 +9,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.3.1.dev1] — 2026-10-16
13
+
14
+ ### Security & Hardening
15
+ - **Path Traversal Protection**: Enforced strict directory boundary containment across all file discovery and inspection tools (`find_unmapped_files`, `analyze_module`, `ki_search`, and custom agent paths) to prevent prefix-based sandbox traversal attacks.
16
+ - **Secured Git Operations (`git_diff_secured`)**: Added hardened diff inspection tool protecting against flag injection (`--output`, etc.), shell command chaining, and repository boundary escapes.
17
+ - **Restricted File Modifications**: Extended write protection to block direct modifications of shell/executable scripts (`.sh`, `.bash`, `.zsh`, `.js`, `.ts`, `.vbs`, `.wsf`) and internal system state files (`doc_config.json`, `ki_config.json`, `config.json`, `doc_state.json`) via direct file editing tools.
18
+
19
+ ### Changed
20
+ - **Unified Tool Dispatch Schema**: In compact facade mode, dynamically bind `ki_call` and `ki_mutate` tool validation schemas (`enum` and `description`) directly to tool `readOnlyHint` annotations, eliminating dispatch conflicts and preventing model confusion.
21
+ - **Date Format Resilience in `update_last_verified`**: Extended `update_last_verified` to automatically detect and update all supported date notation variants (HTML comment `<!-- last_verified: YYYY-MM-DD -->`, YAML frontmatter `last_verified: "YYYY-MM-DD"`, and Markdown bold `**Last verified**: YYYY-MM-DD`), and automatically insert the verification tag if missing.
22
+ - **Workflow Instructions Alignment**: Synchronized tool arguments and signatures across `/expand-knowledge` and `/sync-knowledge` instructions, ensuring seamless execution in both compact facade and full tool modes.
23
+ - **Daily MCP Log Rotation**: Server diagnostics now rotate daily (`mcp_YYYY-MM-DD.log`) with payload truncation to prevent runaway disk usage during high-frequency sessions.
24
+
25
+ ## [2.3.0] — 2026-10-11
26
+
27
+ ### Added
28
+ - **Unified Cascade Path Resolution (`resolve_know_path`)**: `read_know_file`, `write_know_file`, and `edit_know_file` now share `ki_read`'s smart cascade resolver. AI agents can access and edit Knowledge Items using flat basenames (e.g. `KI_data_persistence.md`) without requiring explicit `knowledge/` directory prefixes.
29
+ - **Smart Directory Placement for New KIs**: `write_know_file` now updates existing files in place across `knowledge/` rather than creating duplicates in the sandbox root, and automatically routes newly created flat KI files (`KI_*.md`, `*.ki.md`) into `<jail>/knowledge/`.
30
+ - **ADR Name & Prefix Tolerance**: `resolve_know_path` and `ki_read` gracefully handle variations with or without `ADR_`/`adr_`/`ADR-` prefixes (e.g., resolving `ADR_002_node_status_queue_and_ui_feedback.md` to `002_node_status_queue_and_ui_feedback.md`) as well as numeric ADR IDs.
31
+ - **Accurate Document Listings**: `get_ki_list_table()` dynamically reconciles registered entries in `doc_config.json` against filesystem reality, outputting exact on-disk filenames in document overviews.
32
+ - **Comprehensive Project Overview**: `ki_instructions(document="knowledge-items")`, prompts, and `ki://knowledge-items.md` now present both Knowledge Items and Architecture Decision Records (ADRs) tables together when ADRs exist.
33
+
34
+ ### Highlights from 2.3.0 Development Cycle
35
+ - Compact Facade Mode (`--tool-mode compact`) reducing MCP tools footprint from 34 schemas to 6 tools (`ki_instructions`, `ki_search`, `ki_read`, `ki_tools`, `ki_call`, `ki_mutate`), saving thousands of context tokens.
36
+ - Native BM25 search engine (`ki_search`) with bilingual EN/RU stemming and field-weighted ranking.
37
+ - Token-efficient reader with section filtering and character offset pagination (`ki_read`, `read_know_file`).
38
+ - Batch ADR superseding (`create_adr`) with automatic status transitions, warning banners, git/environment snapshots, and quality sections (`rejected_alternatives`, `invariants`, `verification`).
39
+ - Automated drift analysis (`check_ki_drift`) and Python AST pre-enrichment during scaffolding.
40
+
41
+ ### Added
42
+ - `create_adr` batch `supersedes` support: accept a list of previous ADR IDs (`supersedes: ["001", "002"]`) to supersede multiple older records simultaneously; automatically updates deprecated ADR files with warning banners and `Superseded by ADR XXX` statuses, updates `doc_config.json`, and embeds bi-directional cross-links in the generated ADR.
43
+ - Automated contextual provenance in `create_adr`: deterministically captures Git HEAD commit hash, current branch, and author, as well as an environment snapshot (operating system, Python runtime, and `ki-manager` version) without LLM overhead or prompt tokens.
44
+ - Automated Scope & Knowledge Graph linking in `create_adr`: automatically discovers modified project files from Git or accepts explicit `affected_files`, correlates them with Knowledge Items in `doc_config.json`, and records them in the ADR's `depends_on` dependencies list.
45
+ - Structured ADR quality sections: added support for `rejected_alternatives` (documenting abandoned paths and failed attempts to prevent regression loops), `invariants` (enforceable MUST / MUST NOT rules for future AI sessions), and `verification` (automated test commands or validation checklists).
46
+ - `check_ki_drift` MCP tool & CLI script: automated discrepancy detection between Knowledge Items and the codebase, flagging dead symbols, unmapped public symbols, missing `depends_on` files, broken `[[KI_*.md]]` wiki-links, and Git date discrepancies against `last_verified`.
47
+ - AST pre-enrichment in `generate_ki_scaffolds`: parses Python AST to pre-populate module docstrings into `## Overview`, class/function docstrings into the `Key Components` table, signatures, detected environment variables, and discovered test commands from `tests/`.
48
+ - Canonical KI template featuring `## Entry Points & Public API` and `## Testing & Verification` sections, along with an updated canonical ADR template.
49
+ - Integrated `check_ki_drift` into the `/sync-knowledge` workflow and the `coverage_analysis` tool group in facade mode.
50
+
51
+ ### Changed
52
+ - Updated `/create-adr` workflow instructions and tool schemas with the new ADR parameters across both full and compact tool modes.
53
+ - Aligned KI structures across `scaffold-knowledge.md`, `expand-knowledge.md`, `sync-knowledge.md`, and scaffold generators to the canonical template.
54
+ - Made `finalize_ki_scaffolds` summary extraction resilient and backwards-compatible with legacy `## Purpose` and `## What it Does` headers.
55
+ - Cleaned up duplicate local `knowledge/KI_template.md` in favor of package template.
56
+
12
57
  ## [2.3.0.dev2] — 2026-10-06
13
58
 
14
59
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ki-manager
3
- Version: 2.3.0.dev2
3
+ Version: 2.3.1.dev1
4
4
  Summary: Knowledge Item (KI) management MCP server for AI-assisted development
5
5
  Project-URL: Homepage, https://github.com/Laeryid/KI-base
6
6
  Project-URL: Repository, https://github.com/Laeryid/KI-base
@@ -82,7 +82,10 @@ It provides:
82
82
  {
83
83
  "mcpServers": {
84
84
  "ki-manager": {
85
- "command": "ki-manager"
85
+ "command": "ki-manager",
86
+ "args": ["--tool-mode", "compact"],
87
+ "directTools": true,
88
+ "lifecycle": "eager"
86
89
  }
87
90
  }
88
91
  }
@@ -37,7 +37,10 @@ It provides:
37
37
  {
38
38
  "mcpServers": {
39
39
  "ki-manager": {
40
- "command": "ki-manager"
40
+ "command": "ki-manager",
41
+ "args": ["--tool-mode", "compact"],
42
+ "directTools": true,
43
+ "lifecycle": "eager"
41
44
  }
42
45
  }
43
46
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ki-manager"
7
- version = "2.3.0.dev2"
7
+ version = "2.3.1.dev1"
8
8
  description = "Knowledge Item (KI) management MCP server for AI-assisted development"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -1,3 +1,3 @@
1
1
 
2
2
  """ki-manager — Knowledge Item MCP server for AI-assisted development."""
3
- __version__ = "2.3.0.dev2"
3
+ __version__ = "2.3.1.dev1"
@@ -20,10 +20,10 @@ _WORKFLOWS_DIR = _PACKAGE_DIR / "workflows"
20
20
 
21
21
 
22
22
  def _parse_frontmatter(text: str) -> tuple[dict, str]:
23
- """Парсит YAML frontmatter из маркдаун-файла.
23
+ """Parses YAML frontmatter from a markdown file.
24
24
 
25
- Возвращает (meta_dict, body_text).
26
- Если frontmatter нет — возвращает ({}, text).
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
- # Добавляем путь к скриптам, чтобы импортировать ki_utils
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
- """Возвращает мапу {путь_к_файлу: имя_KI}"""
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,8 +30,11 @@ 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
+ if not ki_utils.is_subpath(abs_target, project_root):
36
+ print(f"Error: Path '{target_path}' escapes project root.")
37
+ return
35
38
 
36
39
  if not os.path.exists(abs_target):
37
40
  print(f"Error: Path {target_path} not found.")
@@ -87,7 +90,7 @@ def analyze_path(target_path: str, recursive: bool = False):
87
90
  continue
88
91
 
89
92
  def main():
90
- # Исправление кодировки для Windows при прямом запуске
93
+ # Fix encoding for Windows when run directly
91
94
  if sys.platform == "win32":
92
95
  import io
93
96
  sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
@@ -65,7 +65,7 @@ def load_ki_files() -> list:
65
65
  ki_dir = os.path.join(get_knowledge_root(), "knowledge")
66
66
  if not os.path.isdir(ki_dir):
67
67
  return []
68
- return [f for f in os.listdir(ki_dir) if f.startswith("KI_") and f.endswith(".md")]
68
+ return [f for f in os.listdir(ki_dir) if ki_utils.is_ki_filename(f)]
69
69
 
70
70
 
71
71
  # ─── Metrics ──────────────────────────────────────────────────────────────────
@@ -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(f for f in knowledge_dir.glob("*.md") if ki_utils.is_ki_filename(f.name)):
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()
@@ -70,7 +70,7 @@ def finalize_scaffolds(dry_run: bool = False) -> None:
70
70
  skipped_clean = 0
71
71
 
72
72
  for fname in sorted(os.listdir(ki_dir)):
73
- if not (fname.startswith("KI_") and fname.endswith(".md")):
73
+ if not ki_utils.is_ki_filename(fname):
74
74
  continue
75
75
 
76
76
  ki_path = os.path.join(ki_dir, fname)
@@ -5,7 +5,7 @@ import json
5
5
  from pathlib import Path
6
6
  from typing import List
7
7
 
8
- # Добавляем путь к ki_utils
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. Собираем все файлы, которые уже замаплены в doc_config.json
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,8 +27,10 @@ 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
+ if not ki_utils.is_subpath(abs_target, project_root):
33
+ return []
32
34
  if not os.path.exists(abs_target):
33
35
  return []
34
36