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.
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/.gitignore +3 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/CHANGELOG.md +45 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/PKG-INFO +5 -2
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/README.md +4 -1
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/pyproject.toml +1 -1
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/__init__.py +1 -1
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/cli.py +3 -3
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/analyze_module.py +7 -4
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/audit_coverage.py +1 -1
- ki_manager-2.3.1.dev1/src/ki_manager/scripts/check_ki_drift.py +382 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/finalize_ki_scaffolds.py +3 -3
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/find_unmapped_files.py +5 -3
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/generate_ki_scaffolds.py +283 -32
- ki_manager-2.3.1.dev1/src/ki_manager/scripts/git_diff_secured.py +81 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_search.py +2 -2
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_utils.py +58 -3
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/knowledge_engine.py +5 -4
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/sync_agents_md.py +8 -4
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/server.py +609 -157
- ki_manager-2.3.1.dev1/src/ki_manager/templates/KI_template.md +27 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/tools/facade.py +47 -2
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/tools/scaffold.py +14 -9
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/create-adr.md +7 -5
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/expand-knowledge.md +14 -4
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/scaffold-knowledge.md +7 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/workflows/sync-knowledge.md +20 -11
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/LICENSE +0 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/add_ki_to_config.py +0 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/generate_dir_index.py +0 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_dependency_analyzer.py +0 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/ki_graph_visualize.py +0 -0
- {ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/tools/__init__.py +0 -0
|
@@ -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.
|
|
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
|
}
|
|
@@ -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,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
|
-
#
|
|
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
|
|
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()
|
{ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/finalize_ki_scaffolds.py
RENAMED
|
@@ -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
|
|
73
|
+
if not ki_utils.is_ki_filename(fname):
|
|
74
74
|
continue
|
|
75
75
|
|
|
76
76
|
ki_path = os.path.join(ki_dir, fname)
|
{ki_manager-2.3.0.dev2 → ki_manager-2.3.1.dev1}/src/ki_manager/scripts/find_unmapped_files.py
RENAMED
|
@@ -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,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
|
|