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.
@@ -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("</", "&lt;/")
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codigest = codigest.main:app