codigest 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codigest/__init__.py +0 -0
- codigest/commands/diff.py +84 -0
- codigest/commands/digest.py +88 -0
- codigest/commands/init.py +87 -0
- codigest/commands/scan.py +142 -0
- codigest/commands/semdiff.py +131 -0
- codigest/commands/tree.py +110 -0
- codigest/config.py +0 -0
- codigest/core/git_ops.py +73 -0
- codigest/core/processor.py +31 -0
- codigest/core/prompts.py +164 -0
- codigest/core/scanner.py +99 -0
- codigest/core/semdiff.py +175 -0
- codigest/core/shadow.py +171 -0
- codigest/core/structure.py +48 -0
- codigest/core/tags.py +75 -0
- codigest/core/tokenizer.py +14 -0
- codigest/main.py +34 -0
- codigest-0.3.0.dist-info/METADATA +145 -0
- codigest-0.3.0.dist-info/RECORD +22 -0
- codigest-0.3.0.dist-info/WHEEL +4 -0
- codigest-0.3.0.dist-info/entry_points.txt +2 -0
codigest/core/shadow.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Anchor Engine.
|
|
3
|
+
"""
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
class ContextAnchor:
|
|
11
|
+
def __init__(self, root_path: Path):
|
|
12
|
+
self.root = root_path
|
|
13
|
+
self.anchor_dir = root_path / ".codigest" / "anchor"
|
|
14
|
+
self.git_dir = self.anchor_dir / ".git"
|
|
15
|
+
|
|
16
|
+
def has_history(self) -> bool:
|
|
17
|
+
"""Checks if a valid anchor (git repo with commits) exists."""
|
|
18
|
+
return self.git_dir.exists() and (self.anchor_dir / ".git" / "HEAD").exists()
|
|
19
|
+
|
|
20
|
+
def _run_git(self, args: list[str], cwd: Path = None, check=True) -> str:
|
|
21
|
+
target_dir = cwd or self.anchor_dir
|
|
22
|
+
cmd = ["git"] + args
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
cmd, cwd=target_dir, capture_output=True, text=True, encoding='utf-8', errors='replace'
|
|
25
|
+
)
|
|
26
|
+
if check and result.returncode != 0:
|
|
27
|
+
logger.debug(f"Shadow Git Warning: {result.stderr}")
|
|
28
|
+
return (result.stdout or "").strip()
|
|
29
|
+
|
|
30
|
+
def update(self, source_files: list[Path]):
|
|
31
|
+
if not self.git_dir.exists():
|
|
32
|
+
self.anchor_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
self._run_git(["init"])
|
|
34
|
+
self._run_git(["config", "user.email", "codigest@ai"])
|
|
35
|
+
self._run_git(["config", "user.name", "Context Manager"])
|
|
36
|
+
self._run_git(["config", "core.autocrlf", "false"])
|
|
37
|
+
self._run_git(["config", "gc.auto", "0"])
|
|
38
|
+
|
|
39
|
+
for item in self.anchor_dir.iterdir():
|
|
40
|
+
if item.name == ".git":
|
|
41
|
+
continue
|
|
42
|
+
if item.is_dir():
|
|
43
|
+
shutil.rmtree(item)
|
|
44
|
+
else:
|
|
45
|
+
item.unlink()
|
|
46
|
+
|
|
47
|
+
for src in source_files:
|
|
48
|
+
if ".git" in src.parts:
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
rel = src.relative_to(self.root)
|
|
52
|
+
dest = self.anchor_dir / rel
|
|
53
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
shutil.copy2(src, dest)
|
|
55
|
+
except Exception:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
self._run_git(["add", "."])
|
|
59
|
+
if self._run_git(["status", "--porcelain"]):
|
|
60
|
+
self._run_git(["commit", "-m", f"Snapshot: {int(time.time())}"])
|
|
61
|
+
logger.info("Context anchor updated.")
|
|
62
|
+
|
|
63
|
+
def get_changes(self, current_files: list[Path]) -> str:
|
|
64
|
+
if not self.git_dir.exists():
|
|
65
|
+
return ""
|
|
66
|
+
|
|
67
|
+
temp_current = self.root / ".codigest" / "temp_diff_current"
|
|
68
|
+
temp_baseline = self.root / ".codigest" / "temp_diff_baseline"
|
|
69
|
+
|
|
70
|
+
for d in [temp_current, temp_baseline]:
|
|
71
|
+
if d.exists():
|
|
72
|
+
shutil.rmtree(d)
|
|
73
|
+
d.mkdir(parents=True)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
current_rel_paths = set()
|
|
77
|
+
for src in current_files:
|
|
78
|
+
if ".git" in src.parts:
|
|
79
|
+
continue
|
|
80
|
+
try:
|
|
81
|
+
rel = src.relative_to(self.root)
|
|
82
|
+
current_rel_paths.add(rel)
|
|
83
|
+
dest = temp_current / rel
|
|
84
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
shutil.copy2(src, dest)
|
|
86
|
+
except Exception:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
subprocess.run(
|
|
90
|
+
["git", "--git-dir", str(self.git_dir), "--work-tree", str(temp_baseline), "checkout", "HEAD", "--", "."],
|
|
91
|
+
capture_output=True, check=False
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
self._prune_ignored_files(temp_baseline, current_rel_paths)
|
|
95
|
+
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
["git", "diff", "--no-index", "--no-prefix", "temp_diff_baseline", "temp_diff_current"],
|
|
98
|
+
cwd=self.root / ".codigest",
|
|
99
|
+
capture_output=True, text=True, encoding='utf-8', errors='replace'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
diff_text = (result.stdout or "")
|
|
103
|
+
diff_text = diff_text.replace("temp_diff_baseline/", "").replace("temp_diff_current/", "")
|
|
104
|
+
return diff_text
|
|
105
|
+
|
|
106
|
+
finally:
|
|
107
|
+
for d in [temp_current, temp_baseline]:
|
|
108
|
+
if d.exists():
|
|
109
|
+
shutil.rmtree(d)
|
|
110
|
+
|
|
111
|
+
def _prune_ignored_files(self, baseline_dir: Path, valid_rel_paths: Set[Path]):
|
|
112
|
+
for file_path in baseline_dir.rglob("*"):
|
|
113
|
+
if file_path.is_file() and ".git" not in file_path.parts:
|
|
114
|
+
try:
|
|
115
|
+
rel_path = file_path.relative_to(baseline_dir)
|
|
116
|
+
if rel_path in valid_rel_paths:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
real_file = self.root / rel_path
|
|
120
|
+
if real_file.exists():
|
|
121
|
+
file_path.unlink()
|
|
122
|
+
except Exception:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
def get_last_update_time(self) -> str:
|
|
126
|
+
if not self.git_dir.exists():
|
|
127
|
+
return "Never"
|
|
128
|
+
try:
|
|
129
|
+
return self._run_git(["log", "-1", "--format=%cr"])
|
|
130
|
+
except:
|
|
131
|
+
return "Unknown"
|
|
132
|
+
|
|
133
|
+
def read_anchor_file(self, rel_path: Path) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Reads the content of a file from the Shadow Git HEAD (Baseline).
|
|
136
|
+
Used for comparing Old vs New AST.
|
|
137
|
+
"""
|
|
138
|
+
if not self.git_dir.exists():
|
|
139
|
+
return ""
|
|
140
|
+
|
|
141
|
+
# Git expects forward slashes
|
|
142
|
+
git_path = rel_path.as_posix()
|
|
143
|
+
|
|
144
|
+
# git show HEAD:path/to/file
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
["git", "--git-dir", str(self.git_dir), "--work-tree", str(self.anchor_dir), "show", f"HEAD:{git_path}"],
|
|
147
|
+
capture_output=True, text=True, encoding='utf-8', errors='replace'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if result.returncode != 0:
|
|
151
|
+
# File might be new (not in anchor), return empty string
|
|
152
|
+
return ""
|
|
153
|
+
|
|
154
|
+
return result.stdout
|
|
155
|
+
|
|
156
|
+
def get_changed_files(self, current_files: list[Path]) -> list[Path]:
|
|
157
|
+
"""Returns list of paths that have text diffs (Modified/Added/Deleted)."""
|
|
158
|
+
raw_diff = self.get_changes(current_files)
|
|
159
|
+
paths = set()
|
|
160
|
+
for line in raw_diff.splitlines():
|
|
161
|
+
# diff --git a/src/main.py b/src/main.py
|
|
162
|
+
if line.startswith("diff --git"):
|
|
163
|
+
parts = line.split()
|
|
164
|
+
# Parse paths (rough parsing, robust enough for standard names)
|
|
165
|
+
# usually parts[-1] is b/path, parts[-2] is a/path
|
|
166
|
+
if len(parts) >= 4:
|
|
167
|
+
# Clean "a/" or "b/"
|
|
168
|
+
p = parts[-1]
|
|
169
|
+
if p.startswith("b/") or p.startswith("a/"): p = p[2:]
|
|
170
|
+
paths.add(self.root / p)
|
|
171
|
+
return sorted(list(paths))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
def generate_toon(paths: list[Path], root_dir: Path) -> str:
|
|
4
|
+
"""(Phase 2: Digest용) TOON 포맷 생성기 (Placeholder)"""
|
|
5
|
+
return "# TOON generation logic here"
|
|
6
|
+
|
|
7
|
+
def generate_ascii_tree(paths: list[Path], root_dir: Path) -> str:
|
|
8
|
+
"""
|
|
9
|
+
[Legacy Style] Generates a standard ASCII directory tree.
|
|
10
|
+
Ref: Ported from original codigest prototype.
|
|
11
|
+
"""
|
|
12
|
+
lines = []
|
|
13
|
+
# Set for fast lookup
|
|
14
|
+
path_set = set(paths)
|
|
15
|
+
|
|
16
|
+
def _walk(current_path: Path, prefix: str = ""):
|
|
17
|
+
try:
|
|
18
|
+
# 현재 디렉토리의 모든 항목 가져오기
|
|
19
|
+
all_items = sorted(current_path.iterdir(), key=lambda x: x.name)
|
|
20
|
+
except PermissionError:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
# 필터링: 스캔된 파일이거나, 스캔된 파일을 포함하는 디렉토리여야 함
|
|
24
|
+
filtered_items = []
|
|
25
|
+
for item in all_items:
|
|
26
|
+
if item.is_file():
|
|
27
|
+
if item in path_set:
|
|
28
|
+
filtered_items.append(item)
|
|
29
|
+
elif item.is_dir():
|
|
30
|
+
# 디렉토리 안에 유효한 파일이 하나라도 있는지 확인 (Deep Check)
|
|
31
|
+
# 성능 최적화를 위해 문자열 startswith 체크
|
|
32
|
+
item_str = str(item)
|
|
33
|
+
if any(str(p).startswith(item_str) for p in path_set):
|
|
34
|
+
filtered_items.append(item)
|
|
35
|
+
|
|
36
|
+
count = len(filtered_items)
|
|
37
|
+
for i, item in enumerate(filtered_items):
|
|
38
|
+
is_last = (i == count - 1)
|
|
39
|
+
connector = "└── " if is_last else "├── "
|
|
40
|
+
|
|
41
|
+
lines.append(f"{prefix}{connector}{item.name}")
|
|
42
|
+
|
|
43
|
+
if item.is_dir():
|
|
44
|
+
extension = " " if is_last else "│ "
|
|
45
|
+
_walk(item, prefix + extension)
|
|
46
|
+
|
|
47
|
+
_walk(root_dir)
|
|
48
|
+
return "\n".join(lines)
|
codigest/core/tags.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Template Engine using Python Tag Strings.
|
|
3
|
+
Advanced Logic: Structure-Aware Dedent & XML Safety.
|
|
4
|
+
"""
|
|
5
|
+
import html
|
|
6
|
+
import textwrap
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
def _get_common_indent(parts: list[str]) -> int:
|
|
10
|
+
"""Calculates minimum indentation from static template parts."""
|
|
11
|
+
min_indent = None
|
|
12
|
+
for part in parts:
|
|
13
|
+
for line in part.splitlines():
|
|
14
|
+
if not line.strip():
|
|
15
|
+
continue
|
|
16
|
+
indent = len(line) - len(line.lstrip())
|
|
17
|
+
if min_indent is None or indent < min_indent:
|
|
18
|
+
min_indent = indent
|
|
19
|
+
return min_indent or 0
|
|
20
|
+
|
|
21
|
+
def _dedent_line(line: str, indent: int) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Safely removes indentation from a single line.
|
|
24
|
+
"""
|
|
25
|
+
if not line.strip():
|
|
26
|
+
return "\n" if line.endswith("\n") else ""
|
|
27
|
+
|
|
28
|
+
if line.startswith(" " * indent):
|
|
29
|
+
return line[indent:]
|
|
30
|
+
|
|
31
|
+
return line.lstrip(" ")
|
|
32
|
+
|
|
33
|
+
def escape_xml_value(value: Any) -> str:
|
|
34
|
+
"""[XML] Escapes characters for XML safety."""
|
|
35
|
+
raw_value = str(value)
|
|
36
|
+
safe_value = html.escape(raw_value, quote=False)
|
|
37
|
+
if "</" in safe_value:
|
|
38
|
+
safe_value = safe_value.replace("</", "</")
|
|
39
|
+
return safe_value
|
|
40
|
+
|
|
41
|
+
def no_escape(value: Any) -> str:
|
|
42
|
+
"""[Plain] Returns string as-is (for dedent)."""
|
|
43
|
+
return str(value)
|
|
44
|
+
|
|
45
|
+
def _render_structure_aware(template: Any, escape_fn: Callable[[Any], str]) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Core Logic: Dedents based on static structure, then injects processed values.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(template, str):
|
|
50
|
+
return textwrap.dedent(template).strip()
|
|
51
|
+
|
|
52
|
+
static_parts = [p for p in template if isinstance(p, str)]
|
|
53
|
+
common_indent = _get_common_indent(static_parts)
|
|
54
|
+
|
|
55
|
+
result = []
|
|
56
|
+
|
|
57
|
+
for part in template:
|
|
58
|
+
if isinstance(part, str):
|
|
59
|
+
# Dedent static parts
|
|
60
|
+
lines = part.splitlines(keepends=True)
|
|
61
|
+
dedented_lines = [_dedent_line(line, common_indent) for line in lines]
|
|
62
|
+
result.append("".join(dedented_lines))
|
|
63
|
+
else:
|
|
64
|
+
# Inject dynamic value
|
|
65
|
+
result.append(escape_fn(part.value))
|
|
66
|
+
|
|
67
|
+
return "".join(result).lstrip()
|
|
68
|
+
|
|
69
|
+
def xml(template: Any) -> str:
|
|
70
|
+
"""[Python 3.14 Native Tag] XML Generator."""
|
|
71
|
+
return _render_structure_aware(template, escape_xml_value)
|
|
72
|
+
|
|
73
|
+
def dedent(template: Any) -> str:
|
|
74
|
+
"""[Python 3.14 Native Tag] Plain Text Generator."""
|
|
75
|
+
return _render_structure_aware(template, no_escape)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight Token Estimator.
|
|
3
|
+
"""
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
def estimate_tokens(text: str) -> int:
|
|
7
|
+
"""
|
|
8
|
+
Estimates token count based on character length.
|
|
9
|
+
Rule of thumb for GPT-4 (cl100k_base): ~4 characters per token.
|
|
10
|
+
This avoids heavy dependencies like tiktoken while providing a useful metric.
|
|
11
|
+
"""
|
|
12
|
+
if not text:
|
|
13
|
+
return 0
|
|
14
|
+
return math.ceil(len(text) / 4)
|
codigest/main.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry Point with Global Error Handling
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from .commands import init, scan, tree, digest, diff, semdiff
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
name="codigest",
|
|
13
|
+
help="Semantic Context Manager for LLM-assisted Development",
|
|
14
|
+
add_completion=False,
|
|
15
|
+
no_args_is_help=True
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app.add_typer(init.app, name="init")
|
|
19
|
+
app.add_typer(scan.app, name="scan")
|
|
20
|
+
app.add_typer(tree.app, name="tree")
|
|
21
|
+
app.add_typer(digest.app, name="digest")
|
|
22
|
+
app.add_typer(diff.app, name="diff")
|
|
23
|
+
app.add_typer(semdiff.app, name="semdiff")
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
try:
|
|
27
|
+
app()
|
|
28
|
+
except Exception as e:
|
|
29
|
+
error_msg = str(e)
|
|
30
|
+
console.print(f"[bold red]❌ Error:[/bold red] {error_msg}")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codigest
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Human-Centered Codebase Context Manager for LLMs
|
|
5
|
+
Project-URL: Homepage, https://github.com/SJB777/codigest
|
|
6
|
+
Project-URL: Repository, https://github.com/SJB777/codigest
|
|
7
|
+
Project-URL: Issues, https://github.com/SJB777/codigest/issues
|
|
8
|
+
Author-email: Codigest Team <sngd024@google.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Requires-Dist: loguru
|
|
19
|
+
Requires-Dist: pathspec
|
|
20
|
+
Requires-Dist: pyperclip
|
|
21
|
+
Requires-Dist: rich
|
|
22
|
+
Requires-Dist: tomli-w
|
|
23
|
+
Requires-Dist: typer
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Codigest
|
|
27
|
+
|
|
28
|
+
Codigest is a CLI tool designed to extract, structure, and track the context of your codebase for Large Language Models (LLMs).
|
|
29
|
+
|
|
30
|
+
Unlike simple copy-paste tools, Codigest employs a **Context Anchor** system (Shadow Git) to track changes locally without polluting your main version control history. It also features **Semantic Analysis** to detect structural changes in your code.
|
|
31
|
+
|
|
32
|
+
## Core Philosophy
|
|
33
|
+
|
|
34
|
+
* **Read-Only & Safe**: Codigest never modifies your source code. It only reads, analyzes, and formats context.
|
|
35
|
+
* **Context-Aware**: Instead of dumping raw text, it structures code into XML snapshots designed for LLM comprehension.
|
|
36
|
+
* **Session-Based Tracking**: It maintains an internal anchor to track "work-in-progress" changes independently of your Git commits.
|
|
37
|
+
* **Python 3.14 Native**: Built using modern Python features including PEP 750 Tag Strings for safe XML generation.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Requires **Python 3.14+**.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Install via uv
|
|
45
|
+
git clone https://github.com/SJB7777/codigest.git
|
|
46
|
+
cd codigest
|
|
47
|
+
uv tool install .
|
|
48
|
+
````
|
|
49
|
+
|
|
50
|
+
## Workflow & Commands
|
|
51
|
+
|
|
52
|
+
### 1\. Initialization
|
|
53
|
+
|
|
54
|
+
Sets up the `.codigest` directory and captures the initial baseline anchor.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
codigest init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2\. Full Context Snapshot (`scan`)
|
|
61
|
+
|
|
62
|
+
Scans the entire codebase and generates a structured XML snapshot.
|
|
63
|
+
|
|
64
|
+
* **Output:** `.codigest/snapshot.xml`
|
|
65
|
+
* **Use Case:** Providing the LLM with the full project context at the start of a session.
|
|
66
|
+
* **Side Effect:** Updates the internal Context Anchor to the current state.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
codigest scan --m " Python 3.14 project. Do NOT use typing. List/Dict."
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3\. Incremental Changes (`diff`)
|
|
73
|
+
|
|
74
|
+
Tracks text-based changes between the last `scan` and the current working tree.
|
|
75
|
+
|
|
76
|
+
* **Output:** `.codigest/changes.diff`
|
|
77
|
+
* **Use Case:** "I modified 3 files. Here is exactly what changed since the last snapshot."
|
|
78
|
+
* **Note:** This is based on the internal Shadow Git, not your project's Git history.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
codigest diff
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4\. Semantic Analysis (`semdiff`)
|
|
85
|
+
|
|
86
|
+
Analyzes **structural changes** (AST-based) rather than line-by-line text differences.
|
|
87
|
+
|
|
88
|
+
* **Output:** `.codigest/semdiff.xml`
|
|
89
|
+
* **Use Case:** "I refactored the API. Show me which functions were added, removed, or had their signatures changed."
|
|
90
|
+
* **Benefit:** Significantly reduces token usage compared to raw diffs by ignoring formatting/comment changes.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
codigest semdiff
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 5\. Architecture Digest (`digest`)
|
|
97
|
+
|
|
98
|
+
Generates a high-level outline of the project structure (Classes, Functions, Methods only).
|
|
99
|
+
|
|
100
|
+
* **Output:** `.codigest/digest.xml`
|
|
101
|
+
* **Use Case:** "Don't read the implementation details. Just understand the class hierarchy and available methods."
|
|
102
|
+
|
|
103
|
+
<!-- end list -->
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
codigest digest
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
You can customize behavior in `.codigest/config.toml`.
|
|
112
|
+
|
|
113
|
+
```toml
|
|
114
|
+
[filter]
|
|
115
|
+
# Target extensions
|
|
116
|
+
extensions = [".py", ".ts", ".rs", ".md", ".json"]
|
|
117
|
+
|
|
118
|
+
# Exclude patterns (Gitignore syntax)
|
|
119
|
+
exclude_patterns = [
|
|
120
|
+
"*.lock",
|
|
121
|
+
"tests/data/",
|
|
122
|
+
"legacy_code/"
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
[output]
|
|
126
|
+
format = "xml"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Architecture Details
|
|
130
|
+
|
|
131
|
+
**Context Anchor (Shadow Git)**
|
|
132
|
+
Codigest maintains a hidden, lightweight Git repository inside `.codigest/anchor`.
|
|
133
|
+
|
|
134
|
+
* When you run `scan`, the current state is committed to this anchor.
|
|
135
|
+
* When you run `diff`, the tool compares your working directory against this anchor.
|
|
136
|
+
* When you run `semdiff`, the tool parses the AST of the anchor version and the current version to detect logical drifts.
|
|
137
|
+
|
|
138
|
+
**Safety Mechanisms**
|
|
139
|
+
|
|
140
|
+
* **structure-aware dedent:** Ensures XML tags are perfectly aligned regardless of code indentation.
|
|
141
|
+
* **Automatic Exclusion:** Self-referential files (`.codigest/`) are automatically ignored to prevent recursion loops.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT License
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
codigest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
codigest/config.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
codigest/main.py,sha256=xbs8o9NFt6DjF2LtC7vyNXN-3jZjwt_N6MD9yjw892w,799
|
|
4
|
+
codigest/commands/diff.py,sha256=uAJkg5vdNAeJcEJfYtM-cDxgtfm2gAqKiOGtRzXQ4JQ,2939
|
|
5
|
+
codigest/commands/digest.py,sha256=LRJH6sySaHw1et8654Gr25mjfdP-PImT16zKG7fUcnI,3149
|
|
6
|
+
codigest/commands/init.py,sha256=R-wTGP3bmBT2-ZMpd2KQTroY83wyOkFjDdiP-tRdq40,2771
|
|
7
|
+
codigest/commands/scan.py,sha256=m6QLfFnkTI-8C4D1L7BKLZIUTPF3Xp5dOHdhtQjs6Rw,5099
|
|
8
|
+
codigest/commands/semdiff.py,sha256=zRQMdpQMdhM6p9Gto8XC3tFEop-ngICYML_qQ9TaZqg,4896
|
|
9
|
+
codigest/commands/tree.py,sha256=pB1k842h6u-Lt1s_J0rDYq55ffaXdczo0PiR4uAzhTg,3717
|
|
10
|
+
codigest/core/git_ops.py,sha256=AAAlRR-b8DPOctARhtnzT5gqrnVjcJgPJzoKz8nEumk,2340
|
|
11
|
+
codigest/core/processor.py,sha256=1VUtzvnqA_JwZ7M2p3CpwBKxffMGkGOb0wGdHOOe-cE,881
|
|
12
|
+
codigest/core/prompts.py,sha256=FwbrHeFlNxcHm6KziBudFB9sL3Y6UjaLQoL3x7DKXq4,5196
|
|
13
|
+
codigest/core/scanner.py,sha256=R8FIKHkvhiQb6CPpne1gtusXtMddj-28n-IRs3k0gAc,3715
|
|
14
|
+
codigest/core/semdiff.py,sha256=m_fU3q4eMjl-hKdnPzvgyIqHGm25wLPoKTGkYlhemqM,6047
|
|
15
|
+
codigest/core/shadow.py,sha256=NqDkJwtyE9PtI_4MNn3DgthmsmCbIE0PSOxuP9PmVWY,6493
|
|
16
|
+
codigest/core/structure.py,sha256=u7qNGD-Kr1UcaBkAixkQLo2uJgfZl5qxZf-gwONe6FY,1828
|
|
17
|
+
codigest/core/tags.py,sha256=BmuTkAdq79NnkKCq4QFRB24bhq0PebX6uvUx9dK8tag,2395
|
|
18
|
+
codigest/core/tokenizer.py,sha256=wuhtOyU0LEHt5K2_GcJm0smxBj-gu3bf4Df6msb7W4k,378
|
|
19
|
+
codigest-0.3.0.dist-info/METADATA,sha256=DJA0yvvxayUsiW8E_j9b951ck-DCrPFAba2yqK8ez-I,4649
|
|
20
|
+
codigest-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
21
|
+
codigest-0.3.0.dist-info/entry_points.txt,sha256=0WsGyQpgl2_MxzxR_KUSmDBSbIBu4V-TL-XzgWPORcc,47
|
|
22
|
+
codigest-0.3.0.dist-info/RECORD,,
|