ki-manager 2.3.1__tar.gz → 2.3.2.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.1 → ki_manager-2.3.2.dev1}/CHANGELOG.md +11 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/PKG-INFO +1 -1
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/pyproject.toml +1 -1
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/__init__.py +1 -1
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/check_ki_drift.py +70 -14
- ki_manager-2.3.2.dev1/src/ki_manager/scripts/find_unmapped_files.py +143 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_utils.py +59 -17
- ki_manager-2.3.1/src/ki_manager/scripts/find_unmapped_files.py +0 -101
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/.gitignore +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/LICENSE +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/README.md +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/cli.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/add_ki_to_config.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/analyze_module.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/audit_coverage.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/finalize_ki_scaffolds.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/generate_dir_index.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/generate_ki_scaffolds.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/git_diff_secured.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_dependency_analyzer.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_graph_visualize.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_search.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/knowledge_engine.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/sync_agents_md.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/server.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/templates/KI_template.md +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/tools/__init__.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/tools/facade.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/tools/scaffold.py +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/workflows/create-adr.md +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/workflows/expand-knowledge.md +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/workflows/scaffold-knowledge.md +0 -0
- {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/workflows/sync-knowledge.md +0 -0
|
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [2.3.2.dev1] — 2026-10-20
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **`find_unmapped_files` Traversal & Scoping**:
|
|
16
|
+
- Automatically loads and enforces `.gitignore` patterns and `doc_config.json` `exclude` rules (in addition to `exclude_patterns`) during repository discovery.
|
|
17
|
+
- When scanning repository root by default, scopes file discovery to root segments of configured `tracked_modules`, avoiding timeouts and runaway directory walks across large external data trees.
|
|
18
|
+
- Made the CLI `path` argument optional, defaulting to root / tracked modules.
|
|
19
|
+
- **`check_ki_drift` Markdown Table Header Validation**:
|
|
20
|
+
- Validates Markdown table headers explicitly for file path columns (`File`, `Path`, `Source`, `Location`) before inspecting cells for broken paths.
|
|
21
|
+
- Tables without file columns (e.g. `| Tool | Access | Purpose |`) are cleanly bypassed, eliminating false-positive `Missing file: Access` errors.
|
|
22
|
+
|
|
12
23
|
## [2.3.1] — 2026-10-18
|
|
13
24
|
|
|
14
25
|
### Security & Hardening
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ki-manager
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2.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
|
|
@@ -76,31 +76,87 @@ def parse_ki_file(ki_path: Path) -> Dict[str, Any]:
|
|
|
76
76
|
|
|
77
77
|
# Extract Key Components table symbols
|
|
78
78
|
# Expected table format: | `SymbolName` | `rel/path/file.ext` | Purpose |
|
|
79
|
+
# Only tables that explicitly contain a File/Path column header are inspected.
|
|
79
80
|
table_symbols: List[Dict[str, str]] = []
|
|
80
81
|
in_key_components = False
|
|
81
|
-
|
|
82
|
+
pending_header: Optional[List[str]] = None
|
|
83
|
+
current_table_active = False
|
|
84
|
+
sym_idx = -1
|
|
85
|
+
file_idx = -1
|
|
82
86
|
|
|
83
87
|
for line in lines:
|
|
84
88
|
if line.startswith("## Key Components"):
|
|
85
89
|
in_key_components = True
|
|
90
|
+
pending_header = None
|
|
91
|
+
current_table_active = False
|
|
86
92
|
continue
|
|
87
93
|
elif in_key_components and line.startswith("## "):
|
|
88
94
|
in_key_components = False
|
|
95
|
+
pending_header = None
|
|
96
|
+
current_table_active = False
|
|
89
97
|
continue
|
|
90
98
|
|
|
91
|
-
if in_key_components
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
if not in_key_components:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
stripped = line.strip()
|
|
103
|
+
if not stripped.startswith("|"):
|
|
104
|
+
pending_header = None
|
|
105
|
+
current_table_active = False
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
raw_cells = [c.strip() for c in stripped.strip("|").split("|")]
|
|
109
|
+
# Check if separator row (e.g. |---|---| or |:---|---:|)
|
|
110
|
+
is_separator = (
|
|
111
|
+
len(raw_cells) > 0
|
|
112
|
+
and all(re.match(r"^:?-{2,}:?$", c) for c in raw_cells if c)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if is_separator:
|
|
116
|
+
if pending_header:
|
|
117
|
+
file_idx = -1
|
|
118
|
+
for i, col in enumerate(pending_header):
|
|
119
|
+
col_lower = col.lower()
|
|
120
|
+
if any(w in col_lower for w in ["file", "path", "source", "location"]):
|
|
121
|
+
file_idx = i
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
sym_idx = -1
|
|
125
|
+
for i, col in enumerate(pending_header):
|
|
126
|
+
if i == file_idx:
|
|
127
|
+
continue
|
|
128
|
+
col_lower = col.lower()
|
|
129
|
+
if any(w in col_lower for w in ["class", "function", "symbol", "component", "name", "element", "method", "tool"]):
|
|
130
|
+
sym_idx = i
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
if sym_idx == -1 and file_idx != -1:
|
|
134
|
+
sym_idx = 0 if file_idx != 0 else 1
|
|
135
|
+
|
|
136
|
+
current_table_active = (file_idx != -1 and sym_idx != -1)
|
|
137
|
+
pending_header = None
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if pending_header is None and not current_table_active:
|
|
141
|
+
# Possible header row
|
|
142
|
+
pending_header = raw_cells
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if current_table_active:
|
|
146
|
+
if len(raw_cells) > max(sym_idx, file_idx):
|
|
147
|
+
sym_val = raw_cells[sym_idx].strip(" `").strip()
|
|
148
|
+
file_val = raw_cells[file_idx].strip(" `").strip()
|
|
149
|
+
header_keywords = {"component", "file", "path", "purpose", "class", "function", "description", "tool", "access"}
|
|
150
|
+
if (
|
|
151
|
+
sym_val
|
|
152
|
+
and file_val
|
|
153
|
+
and not sym_val.startswith("—")
|
|
154
|
+
and not file_val.startswith("—")
|
|
155
|
+
and not file_val.startswith("-")
|
|
156
|
+
and sym_val.lower() not in header_keywords
|
|
157
|
+
and file_val.lower() not in header_keywords
|
|
158
|
+
):
|
|
159
|
+
table_symbols.append({"symbol": sym_val, "file": file_val})
|
|
104
160
|
|
|
105
161
|
# Extract wiki-links [[KI_*.md]]
|
|
106
162
|
wiki_links = []
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
# Add path to ki_utils
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
+
import ki_utils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _scan_path(target_path: str, project_root: str, mapped_paths: set, exclude_patterns: set, ignored_exts: set) -> List[str]:
|
|
14
|
+
abs_target = os.path.abspath(os.path.join(project_root, target_path))
|
|
15
|
+
if not ki_utils.is_subpath(abs_target, project_root) or not os.path.exists(abs_target):
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
unmapped = []
|
|
19
|
+
if os.path.isfile(abs_target):
|
|
20
|
+
rel_to_project = os.path.relpath(abs_target, project_root).replace("\\", "/")
|
|
21
|
+
fname = os.path.basename(abs_target)
|
|
22
|
+
if fname.startswith(".") or any(fname.endswith(ext) for ext in ignored_exts):
|
|
23
|
+
return []
|
|
24
|
+
if ki_utils.should_exclude(fname, rel_to_project, exclude_patterns):
|
|
25
|
+
return []
|
|
26
|
+
if abs_target in mapped_paths:
|
|
27
|
+
return []
|
|
28
|
+
for mapped in mapped_paths:
|
|
29
|
+
if os.path.isdir(mapped) and abs_target.startswith(os.path.join(mapped, "")):
|
|
30
|
+
return []
|
|
31
|
+
return [rel_to_project]
|
|
32
|
+
|
|
33
|
+
for root, dirs, files in os.walk(abs_target):
|
|
34
|
+
dirs[:] = [
|
|
35
|
+
d for d in dirs
|
|
36
|
+
if not d.startswith(".")
|
|
37
|
+
and not ki_utils.should_exclude(d, os.path.relpath(os.path.join(root, d), project_root), exclude_patterns)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
for f in files:
|
|
41
|
+
if f.startswith(".") or any(f.endswith(ext) for ext in ignored_exts):
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
full_path = os.path.abspath(os.path.join(root, f))
|
|
45
|
+
rel_to_project = os.path.relpath(full_path, project_root).replace("\\", "/")
|
|
46
|
+
if ki_utils.should_exclude(f, rel_to_project, exclude_patterns):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if full_path in mapped_paths:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
is_parent_mapped = False
|
|
53
|
+
for mapped in mapped_paths:
|
|
54
|
+
if os.path.isdir(mapped):
|
|
55
|
+
if full_path.startswith(os.path.join(mapped, "")):
|
|
56
|
+
is_parent_mapped = True
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
if not is_parent_mapped:
|
|
60
|
+
unmapped.append(rel_to_project)
|
|
61
|
+
|
|
62
|
+
return unmapped
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_unmapped_files(target_path: str = ".") -> List[str]:
|
|
66
|
+
"""Returns a sorted list of unmapped files relative to project root."""
|
|
67
|
+
project_root = ki_utils.get_project_root()
|
|
68
|
+
doc_config = ki_utils.get_doc_config()
|
|
69
|
+
|
|
70
|
+
if not project_root or not doc_config:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
# 1. Collect all files that are already mapped in doc_config.json
|
|
74
|
+
mapped_paths = set()
|
|
75
|
+
knowledge_items = doc_config.get("knowledge_items", {})
|
|
76
|
+
|
|
77
|
+
for ki in knowledge_items.values():
|
|
78
|
+
depends_on = ki.get("depends_on", [])
|
|
79
|
+
for p in depends_on:
|
|
80
|
+
abs_p = os.path.abspath(os.path.join(project_root, p))
|
|
81
|
+
mapped_paths.add(abs_p)
|
|
82
|
+
|
|
83
|
+
ignored_exts = {'.pyc', '.pyo', '.pyd', '.obj', '.dll', '.exe', '.bin'}
|
|
84
|
+
exclude_patterns = ki_utils.get_exclude_patterns()
|
|
85
|
+
|
|
86
|
+
norm_target = (target_path or ".").strip()
|
|
87
|
+
|
|
88
|
+
# If target is root/empty, scope to tracked_modules if defined
|
|
89
|
+
if norm_target in (".", ""):
|
|
90
|
+
tracked_modules = doc_config.get("coverage_settings", {}).get("tracked_modules") or doc_config.get("tracked_modules", [])
|
|
91
|
+
target_dirs = set()
|
|
92
|
+
if isinstance(tracked_modules, list) and tracked_modules:
|
|
93
|
+
for m in tracked_modules:
|
|
94
|
+
mod_path = m[0] if isinstance(m, (list, tuple)) else m
|
|
95
|
+
if isinstance(mod_path, str) and mod_path.strip():
|
|
96
|
+
norm_p = mod_path.replace("\\", "/").strip("/")
|
|
97
|
+
top_segment = norm_p.split("/")[0] if "/" in norm_p else norm_p
|
|
98
|
+
if top_segment:
|
|
99
|
+
target_dirs.add(top_segment)
|
|
100
|
+
|
|
101
|
+
if target_dirs:
|
|
102
|
+
unmapped_set = set()
|
|
103
|
+
for td in sorted(target_dirs):
|
|
104
|
+
unmapped_set.update(_scan_path(td, project_root, mapped_paths, exclude_patterns, ignored_exts))
|
|
105
|
+
return sorted(unmapped_set)
|
|
106
|
+
else:
|
|
107
|
+
return sorted(_scan_path(".", project_root, mapped_paths, exclude_patterns, ignored_exts))
|
|
108
|
+
|
|
109
|
+
return sorted(_scan_path(norm_target, project_root, mapped_paths, exclude_patterns, ignored_exts))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def find_unmapped_files(target_path: str = "."):
|
|
113
|
+
project_root = ki_utils.get_project_root()
|
|
114
|
+
doc_config = ki_utils.get_doc_config()
|
|
115
|
+
|
|
116
|
+
if not project_root or not doc_config:
|
|
117
|
+
print("Error: Could not resolve project root or doc_config.json")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
display_target = (target_path or ".").strip()
|
|
121
|
+
if display_target != ".":
|
|
122
|
+
abs_target = os.path.abspath(os.path.join(project_root, display_target))
|
|
123
|
+
if not os.path.exists(abs_target):
|
|
124
|
+
print(f"Error: Target path '{display_target}' does not exist.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
unmapped = get_unmapped_files(display_target)
|
|
128
|
+
if unmapped:
|
|
129
|
+
print(f"### Unmapped Files in '{display_target}':")
|
|
130
|
+
for f in unmapped:
|
|
131
|
+
print(f"- {f}")
|
|
132
|
+
else:
|
|
133
|
+
print(f"All files in '{display_target}' are already mapped in doc_config.json.")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
if sys.platform == "win32":
|
|
138
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
139
|
+
parser = argparse.ArgumentParser(description="Find files in a directory not covered by any KI in doc_config.json")
|
|
140
|
+
parser.add_argument("path", nargs="?", default=".", help="Relative path from project root to scan (default: tracked_modules or .)")
|
|
141
|
+
args = parser.parse_args()
|
|
142
|
+
|
|
143
|
+
find_unmapped_files(args.path)
|
|
@@ -335,6 +335,34 @@ def get_local_config() -> dict:
|
|
|
335
335
|
return {}
|
|
336
336
|
|
|
337
337
|
|
|
338
|
+
def load_gitignore_patterns(project_root: str = None) -> set:
|
|
339
|
+
"""Parses .gitignore file in project_root if present, returning a set of pattern strings."""
|
|
340
|
+
if not project_root:
|
|
341
|
+
project_root = get_project_root()
|
|
342
|
+
if not project_root or not os.path.isdir(project_root):
|
|
343
|
+
return set()
|
|
344
|
+
|
|
345
|
+
gitignore_path = os.path.join(project_root, ".gitignore")
|
|
346
|
+
if not os.path.isfile(gitignore_path):
|
|
347
|
+
return set()
|
|
348
|
+
|
|
349
|
+
patterns = set()
|
|
350
|
+
try:
|
|
351
|
+
with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
|
|
352
|
+
for line in f:
|
|
353
|
+
line = line.strip()
|
|
354
|
+
if not line or line.startswith("#") or line.startswith("!"):
|
|
355
|
+
continue
|
|
356
|
+
norm = line.replace("\\", "/").rstrip("/")
|
|
357
|
+
if norm.startswith("/"):
|
|
358
|
+
norm = norm.lstrip("/")
|
|
359
|
+
if norm:
|
|
360
|
+
patterns.add(norm)
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
return patterns
|
|
364
|
+
|
|
365
|
+
|
|
338
366
|
DEFAULT_EXCLUDED_DIRS = {
|
|
339
367
|
".git", "__pycache__", "node_modules", ".venv", "venv", "dist", "build",
|
|
340
368
|
".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
@@ -344,33 +372,45 @@ DEFAULT_EXCLUDED_DIRS = {
|
|
|
344
372
|
def get_exclude_patterns() -> set:
|
|
345
373
|
"""
|
|
346
374
|
Returns the set of directory/file exclude patterns.
|
|
347
|
-
Combines default excluded dirs with exclude_patterns from:
|
|
375
|
+
Combines default excluded dirs with exclude_patterns and exclude from:
|
|
348
376
|
1. config.json (machine-specific)
|
|
349
|
-
2. doc_config.json (coverage_settings.exclude_patterns
|
|
377
|
+
2. doc_config.json (coverage_settings.exclude_patterns, coverage_settings.exclude,
|
|
378
|
+
top-level exclude_patterns, top-level exclude)
|
|
350
379
|
3. ki_config.json
|
|
380
|
+
4. .gitignore in project root
|
|
351
381
|
"""
|
|
352
382
|
patterns = set(DEFAULT_EXCLUDED_DIRS)
|
|
353
383
|
|
|
384
|
+
def _add_from_cfg(cfg_dict, key):
|
|
385
|
+
val = cfg_dict.get(key, [])
|
|
386
|
+
if isinstance(val, list):
|
|
387
|
+
for p in val:
|
|
388
|
+
if p:
|
|
389
|
+
patterns.add(str(p))
|
|
390
|
+
elif isinstance(val, str) and val:
|
|
391
|
+
patterns.add(val)
|
|
392
|
+
|
|
354
393
|
# 1. config.json
|
|
355
394
|
local_cfg = get_local_config()
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
patterns.add(str(p))
|
|
395
|
+
_add_from_cfg(local_cfg, "exclude_patterns")
|
|
396
|
+
_add_from_cfg(local_cfg, "exclude")
|
|
359
397
|
|
|
360
398
|
# 2. doc_config.json
|
|
361
399
|
doc_cfg = get_doc_config()
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
400
|
+
_add_from_cfg(doc_cfg, "exclude_patterns")
|
|
401
|
+
_add_from_cfg(doc_cfg, "exclude")
|
|
402
|
+
cov_cfg = doc_cfg.get("coverage_settings", {})
|
|
403
|
+
if isinstance(cov_cfg, dict):
|
|
404
|
+
_add_from_cfg(cov_cfg, "exclude_patterns")
|
|
405
|
+
_add_from_cfg(cov_cfg, "exclude")
|
|
368
406
|
|
|
369
407
|
# 3. ki_config.json
|
|
370
408
|
ki_cfg = get_ki_cfg()
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
409
|
+
_add_from_cfg(ki_cfg, "exclude_patterns")
|
|
410
|
+
_add_from_cfg(ki_cfg, "exclude")
|
|
411
|
+
|
|
412
|
+
# 4. .gitignore
|
|
413
|
+
patterns.update(load_gitignore_patterns())
|
|
374
414
|
|
|
375
415
|
return patterns
|
|
376
416
|
|
|
@@ -389,15 +429,17 @@ def should_exclude(name: str, rel_path: str = "", patterns: set = None) -> bool:
|
|
|
389
429
|
norm_rel = rel_path.replace("\\", "/").strip("/") if rel_path else ""
|
|
390
430
|
|
|
391
431
|
for pat in patterns:
|
|
392
|
-
|
|
432
|
+
norm_pat = pat.replace("\\", "/").strip("/")
|
|
433
|
+
if not norm_pat:
|
|
434
|
+
continue
|
|
435
|
+
if fnmatch.fnmatch(name, norm_pat):
|
|
393
436
|
return True
|
|
394
437
|
if norm_rel:
|
|
395
|
-
norm_pat = pat.replace("\\", "/").strip("/")
|
|
396
438
|
if norm_rel == norm_pat or norm_rel.startswith(norm_pat + "/"):
|
|
397
439
|
return True
|
|
398
440
|
if fnmatch.fnmatch(norm_rel, norm_pat):
|
|
399
441
|
return True
|
|
400
|
-
if any(fnmatch.fnmatch(comp,
|
|
442
|
+
if any(fnmatch.fnmatch(comp, norm_pat) for comp in norm_rel.split("/")):
|
|
401
443
|
return True
|
|
402
444
|
|
|
403
445
|
return False
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import argparse
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import List
|
|
7
|
-
|
|
8
|
-
# Add path to ki_utils
|
|
9
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
-
import ki_utils
|
|
11
|
-
|
|
12
|
-
def get_unmapped_files(target_path: str = ".") -> List[str]:
|
|
13
|
-
"""Returns a sorted list of unmapped files relative to project root."""
|
|
14
|
-
project_root = ki_utils.get_project_root()
|
|
15
|
-
doc_config = ki_utils.get_doc_config()
|
|
16
|
-
|
|
17
|
-
if not project_root or not doc_config:
|
|
18
|
-
return []
|
|
19
|
-
|
|
20
|
-
# 1. Collect all files that are already mapped in doc_config.json
|
|
21
|
-
mapped_paths = set()
|
|
22
|
-
knowledge_items = doc_config.get("knowledge_items", {})
|
|
23
|
-
|
|
24
|
-
for ki in knowledge_items.values():
|
|
25
|
-
depends_on = ki.get("depends_on", [])
|
|
26
|
-
for p in depends_on:
|
|
27
|
-
abs_p = os.path.abspath(os.path.join(project_root, p))
|
|
28
|
-
mapped_paths.add(abs_p)
|
|
29
|
-
|
|
30
|
-
# 2. Scan the target directory
|
|
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 []
|
|
34
|
-
if not os.path.exists(abs_target):
|
|
35
|
-
return []
|
|
36
|
-
|
|
37
|
-
unmapped = []
|
|
38
|
-
ignored_exts = {'.pyc', '.pyo', '.pyd', '.obj', '.dll', '.exe', '.bin'}
|
|
39
|
-
exclude_patterns = ki_utils.get_exclude_patterns()
|
|
40
|
-
|
|
41
|
-
for root, dirs, files in os.walk(abs_target):
|
|
42
|
-
dirs[:] = [
|
|
43
|
-
d for d in dirs
|
|
44
|
-
if not d.startswith(".")
|
|
45
|
-
and not ki_utils.should_exclude(d, os.path.relpath(os.path.join(root, d), project_root), exclude_patterns)
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
for f in files:
|
|
49
|
-
if f.startswith(".") or any(f.endswith(ext) for ext in ignored_exts):
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
full_path = os.path.abspath(os.path.join(root, f))
|
|
53
|
-
rel_to_project = os.path.relpath(full_path, project_root).replace("\\", "/")
|
|
54
|
-
if ki_utils.should_exclude(f, rel_to_project, exclude_patterns):
|
|
55
|
-
continue
|
|
56
|
-
|
|
57
|
-
if full_path in mapped_paths:
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
is_parent_mapped = False
|
|
61
|
-
for mapped in mapped_paths:
|
|
62
|
-
if os.path.isdir(mapped):
|
|
63
|
-
if full_path.startswith(os.path.join(mapped, "")):
|
|
64
|
-
is_parent_mapped = True
|
|
65
|
-
break
|
|
66
|
-
|
|
67
|
-
if not is_parent_mapped:
|
|
68
|
-
unmapped.append(rel_to_project)
|
|
69
|
-
|
|
70
|
-
return sorted(unmapped)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def find_unmapped_files(target_path: str):
|
|
74
|
-
project_root = ki_utils.get_project_root()
|
|
75
|
-
doc_config = ki_utils.get_doc_config()
|
|
76
|
-
|
|
77
|
-
if not project_root or not doc_config:
|
|
78
|
-
print("Error: Could not resolve project root or doc_config.json")
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
abs_target = os.path.abspath(os.path.join(project_root, target_path))
|
|
82
|
-
if not os.path.exists(abs_target):
|
|
83
|
-
print(f"Error: Target path '{target_path}' does not exist.")
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
unmapped = get_unmapped_files(target_path)
|
|
87
|
-
if unmapped:
|
|
88
|
-
print(f"### Unmapped Files in '{target_path}':")
|
|
89
|
-
for f in unmapped:
|
|
90
|
-
print(f"- {f}")
|
|
91
|
-
else:
|
|
92
|
-
print(f"All files in '{target_path}' are already mapped in doc_config.json.")
|
|
93
|
-
|
|
94
|
-
if __name__ == "__main__":
|
|
95
|
-
if sys.platform == "win32":
|
|
96
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
|
97
|
-
parser = argparse.ArgumentParser(description="Find files in a directory not covered by any KI in doc_config.json")
|
|
98
|
-
parser.add_argument("path", help="Relative path from project root to scan")
|
|
99
|
-
args = parser.parse_args()
|
|
100
|
-
|
|
101
|
-
find_unmapped_files(args.path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|