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.
Files changed (33) hide show
  1. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/CHANGELOG.md +11 -0
  2. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/PKG-INFO +1 -1
  3. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/pyproject.toml +1 -1
  4. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/__init__.py +1 -1
  5. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/check_ki_drift.py +70 -14
  6. ki_manager-2.3.2.dev1/src/ki_manager/scripts/find_unmapped_files.py +143 -0
  7. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_utils.py +59 -17
  8. ki_manager-2.3.1/src/ki_manager/scripts/find_unmapped_files.py +0 -101
  9. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/.gitignore +0 -0
  10. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/LICENSE +0 -0
  11. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/README.md +0 -0
  12. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/cli.py +0 -0
  13. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/add_ki_to_config.py +0 -0
  14. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/analyze_module.py +0 -0
  15. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/audit_coverage.py +0 -0
  16. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/finalize_ki_scaffolds.py +0 -0
  17. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/generate_dir_index.py +0 -0
  18. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/generate_ki_scaffolds.py +0 -0
  19. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/git_diff_secured.py +0 -0
  20. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_dependency_analyzer.py +0 -0
  21. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_graph_visualize.py +0 -0
  22. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/ki_search.py +0 -0
  23. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/knowledge_engine.py +0 -0
  24. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/scripts/sync_agents_md.py +0 -0
  25. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/server.py +0 -0
  26. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/templates/KI_template.md +0 -0
  27. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/tools/__init__.py +0 -0
  28. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/tools/facade.py +0 -0
  29. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/tools/scaffold.py +0 -0
  30. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/workflows/create-adr.md +0 -0
  31. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/workflows/expand-knowledge.md +0 -0
  32. {ki_manager-2.3.1 → ki_manager-2.3.2.dev1}/src/ki_manager/workflows/scaffold-knowledge.md +0 -0
  33. {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.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ki-manager"
7
- version = "2.3.1"
7
+ version = "2.3.2.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.1"
3
+ __version__ = "2.3.2.dev1"
@@ -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
- table_row_pattern = re.compile(r"\|\s*`?([A-Za-z0-9_]+)`?\s*\|\s*`?([^`|\s]+)`?\s*\|")
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 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})
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 or top-level 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
- for p in local_cfg.get("exclude_patterns", []):
357
- if p:
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
- for p in doc_cfg.get("exclude_patterns", []):
363
- if p:
364
- patterns.add(str(p))
365
- for p in doc_cfg.get("coverage_settings", {}).get("exclude_patterns", []):
366
- if p:
367
- patterns.add(str(p))
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
- for p in ki_cfg.get("exclude_patterns", []):
372
- if p:
373
- patterns.add(str(p))
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
- if fnmatch.fnmatch(name, pat):
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, pat) for comp in norm_rel.split("/")):
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