codigest 0.3.3__tar.gz → 0.3.5__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.
- {codigest-0.3.3 → codigest-0.3.5}/PKG-INFO +1 -1
- {codigest-0.3.3 → codigest-0.3.5}/pyproject.toml +2 -2
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/commands/diff.py +26 -25
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/commands/digest.py +18 -13
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/commands/scan.py +20 -90
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/commands/semdiff.py +43 -37
- codigest-0.3.5/src/codigest/commands/tree.py +82 -0
- codigest-0.3.5/src/codigest/core/common.py +118 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/prompts.py +2 -2
- codigest-0.3.5/src/codigest/core/resolver.py +151 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/scanner.py +3 -3
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/semdiff.py +2 -2
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/shadow.py +30 -3
- {codigest-0.3.3 → codigest-0.3.5}/uv.lock +5 -1
- codigest-0.3.3/src/codigest/commands/tree.py +0 -108
- codigest-0.3.3/src/codigest/core/resolver.py +0 -107
- {codigest-0.3.3 → codigest-0.3.5}/.github/workflows/publish.yml +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/.gitignore +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/.python-version +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/README.md +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/__init__.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/__main__.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/commands/init.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/config.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/git_ops.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/processor.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/structure.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/tags.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/core/tokenizer.py +0 -0
- {codigest-0.3.3 → codigest-0.3.5}/src/codigest/main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codigest"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.5"
|
|
4
4
|
description = "Human-Centered Codebase Context Manager for LLMs"
|
|
5
5
|
authors = [{name = "Codigest Team", email = "sngd024@google.com"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -36,4 +36,4 @@ cdg = "codigest.main:app"
|
|
|
36
36
|
|
|
37
37
|
[build-system]
|
|
38
38
|
requires = ["hatchling"]
|
|
39
|
-
build-backend = "hatchling.build"
|
|
39
|
+
build-backend = "hatchling.build"
|
|
@@ -4,15 +4,13 @@ from pathlib import Path
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
6
6
|
|
|
7
|
-
from ..core import
|
|
8
|
-
from .scan import _load_config_filters
|
|
7
|
+
from ..core import prompts, shadow, tags, common
|
|
9
8
|
|
|
10
9
|
app = typer.Typer()
|
|
11
10
|
console = Console()
|
|
12
11
|
|
|
13
12
|
@app.callback(invoke_without_command=True)
|
|
14
13
|
def handle(
|
|
15
|
-
# target 인자 추가
|
|
16
14
|
target: Path = typer.Argument(
|
|
17
15
|
Path.cwd(),
|
|
18
16
|
help="Target directory to check diff",
|
|
@@ -24,24 +22,29 @@ def handle(
|
|
|
24
22
|
copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
|
|
25
23
|
save: bool = typer.Option(True, help="Save to .codigest/changes.diff"),
|
|
26
24
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction context"),
|
|
25
|
+
# [추가] resolve 옵션
|
|
26
|
+
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
|
|
27
27
|
):
|
|
28
28
|
"""
|
|
29
29
|
[Context Update] Shows changes since the last 'codigest scan'.
|
|
30
30
|
Useful for updating LLM context without re-uploading everything.
|
|
31
31
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# [1] Context Setup
|
|
33
|
+
ctx = common.get_context(target)
|
|
34
|
+
root_path = ctx.root_path
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
anchor = shadow.ContextAnchor(root_path)
|
|
37
|
+
|
|
38
|
+
# Check Baseline
|
|
36
39
|
last_update = anchor.get_last_update_time()
|
|
37
40
|
if last_update == "Never":
|
|
38
|
-
console.print("[yellow]No scan history found.[/yellow]")
|
|
39
|
-
console.print(" Run [bold cyan]
|
|
41
|
+
console.print("[yellow]⚠️ No scan history found.[/yellow]")
|
|
42
|
+
console.print(" Run [bold cyan]cdg scan[/bold cyan] first to establish a baseline.")
|
|
40
43
|
raise typer.Exit(1)
|
|
41
44
|
|
|
42
45
|
console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
|
|
43
46
|
|
|
44
|
-
# 2
|
|
47
|
+
# [2] Calculate Diff via Context
|
|
45
48
|
with Progress(
|
|
46
49
|
SpinnerColumn(),
|
|
47
50
|
TextColumn("[bold blue]Analyzing changes...[/bold blue]"),
|
|
@@ -49,23 +52,20 @@ def handle(
|
|
|
49
52
|
console=console
|
|
50
53
|
) as progress:
|
|
51
54
|
task = progress.add_task("diff", total=None)
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# Get current valid files (Working Tree)
|
|
57
|
-
current_files = scanner.scan_project(root_path, extensions, extra_ignores)
|
|
58
|
-
|
|
55
|
+
|
|
56
|
+
# ★ Use common context to get files (handles config & resolve)
|
|
57
|
+
current_files = ctx.get_target_files(resolve_deps=resolve)
|
|
58
|
+
|
|
59
59
|
# Compare Working Tree vs Anchor
|
|
60
60
|
diff_content = anchor.get_changes(current_files)
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
progress.update(task, completed=100)
|
|
63
63
|
|
|
64
64
|
if not diff_content.strip():
|
|
65
|
-
console.print("[green]
|
|
65
|
+
console.print("[green]No changes detected since last scan.[/green]")
|
|
66
66
|
return
|
|
67
67
|
|
|
68
|
-
# 3
|
|
68
|
+
# [3] Render
|
|
69
69
|
prompt_engine = prompts.get_engine(root_path)
|
|
70
70
|
try:
|
|
71
71
|
formatted_diff = prompt_engine.render(
|
|
@@ -76,18 +76,19 @@ def handle(
|
|
|
76
76
|
instruction=message
|
|
77
77
|
)
|
|
78
78
|
except Exception:
|
|
79
|
-
# Fallback if template fails
|
|
80
|
-
|
|
79
|
+
# Fallback if template fails (using safe escaping via tags)
|
|
80
|
+
safe_content = tags.escape_xml_value(diff_content)
|
|
81
|
+
formatted_diff = f"<diff>\n{safe_content}\n</diff>"
|
|
81
82
|
|
|
82
|
-
# 4
|
|
83
|
-
console.print(f"[bold green]
|
|
83
|
+
# [4] Output
|
|
84
|
+
console.print(f"[bold green]Changes Detected![/bold green] ({len(formatted_diff)} chars)")
|
|
84
85
|
|
|
85
86
|
if copy:
|
|
86
87
|
pyperclip.copy(formatted_diff)
|
|
87
|
-
console.print("[dim]
|
|
88
|
+
console.print("[dim]Clipboard copied[/dim]")
|
|
88
89
|
|
|
89
90
|
if save:
|
|
90
91
|
out_path = root_path / ".codigest" / "changes.diff"
|
|
91
92
|
out_path.parent.mkdir(exist_ok=True)
|
|
92
93
|
out_path.write_text(formatted_diff, encoding="utf-8")
|
|
93
|
-
console.print(f"[dim]Saved to {out_path}[/dim]")
|
|
94
|
+
console.print(f"[dim]Saved to {out_path}[/dim]")
|
|
@@ -4,10 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
6
6
|
|
|
7
|
-
from ..core import
|
|
8
|
-
|
|
9
|
-
# Reuse config loader
|
|
10
|
-
from .scan import _load_config_filters
|
|
7
|
+
from ..core import structure, prompts, semdiff, tags, tokenizer, common
|
|
11
8
|
|
|
12
9
|
app = typer.Typer()
|
|
13
10
|
console = Console()
|
|
@@ -18,14 +15,17 @@ def handle(
|
|
|
18
15
|
copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
|
|
19
16
|
save: bool = typer.Option(True, help="Save to .codigest/digest.xml"),
|
|
20
17
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
|
|
18
|
+
# [추가]
|
|
19
|
+
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
|
|
21
20
|
):
|
|
22
21
|
"""
|
|
23
22
|
[Architectural View] Summarizes the codebase structure (Classes/Functions only).
|
|
24
23
|
"""
|
|
25
|
-
|
|
24
|
+
# [1] Context Setup
|
|
25
|
+
ctx = common.get_context(target)
|
|
26
|
+
root_path = ctx.root_path
|
|
26
27
|
|
|
27
28
|
prompt_engine = prompts.get_engine(root_path)
|
|
28
|
-
extensions, extra_ignores = _load_config_filters(root_path)
|
|
29
29
|
|
|
30
30
|
with Progress(
|
|
31
31
|
SpinnerColumn(),
|
|
@@ -36,19 +36,24 @@ def handle(
|
|
|
36
36
|
|
|
37
37
|
task = progress.add_task("digest", total=None)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# [2] File Discovery via Context
|
|
40
|
+
files = ctx.get_target_files(ignore_config=False, resolve_deps=resolve)
|
|
41
|
+
|
|
40
42
|
tree_str = structure.generate_ascii_tree(files, root_path)
|
|
41
43
|
|
|
42
44
|
summary_blocks = []
|
|
43
45
|
for file_path in files:
|
|
44
|
-
# Only summarize Python files via AST
|
|
45
46
|
if file_path.suffix in (".py", ".pyi"):
|
|
46
47
|
try:
|
|
47
48
|
content = file_path.read_text(encoding="utf-8")
|
|
48
49
|
summary = semdiff.summarize(content)
|
|
49
50
|
|
|
50
51
|
if summary:
|
|
51
|
-
|
|
52
|
+
try:
|
|
53
|
+
rel_path = file_path.relative_to(root_path).as_posix()
|
|
54
|
+
except ValueError:
|
|
55
|
+
rel_path = f"[EXTERNAL]/{file_path.name}"
|
|
56
|
+
|
|
52
57
|
block = tags.file(rel_path, summary)
|
|
53
58
|
summary_blocks.append(block)
|
|
54
59
|
except Exception:
|
|
@@ -65,20 +70,20 @@ def handle(
|
|
|
65
70
|
instruction=message
|
|
66
71
|
)
|
|
67
72
|
except Exception as e:
|
|
68
|
-
console.print(f"[red]Rendering Failed:[/red] {e}")
|
|
73
|
+
console.print(f"[red]❌ Rendering Failed:[/red] {e}")
|
|
69
74
|
raise typer.Exit(1)
|
|
70
75
|
|
|
71
76
|
progress.update(task, completed=100)
|
|
72
77
|
|
|
73
78
|
token_count = tokenizer.estimate_tokens(digest_content)
|
|
74
|
-
console.print(f"[bold green]
|
|
79
|
+
console.print(f"[bold green]Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
|
|
75
80
|
|
|
76
81
|
if copy:
|
|
77
82
|
pyperclip.copy(digest_content)
|
|
78
|
-
console.print("[dim]Copied to clipboard[/dim]")
|
|
83
|
+
console.print("[dim]📋 Copied to clipboard[/dim]")
|
|
79
84
|
|
|
80
85
|
if save:
|
|
81
86
|
out_path = root_path / ".codigest" / "digest.xml"
|
|
82
87
|
out_path.parent.mkdir(exist_ok=True)
|
|
83
88
|
out_path.write_text(digest_content, encoding="utf-8")
|
|
84
|
-
console.print(f"[dim]Saved to {out_path}[/dim]")
|
|
89
|
+
console.print(f"[dim]💾 Saved to {out_path}[/dim]")
|
|
@@ -1,43 +1,15 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
import tomllib
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
from rich.console import Console
|
|
5
4
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
6
5
|
from rich.panel import Panel
|
|
7
6
|
from rich.filesize import decimal
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
from ..core import scanner, structure, tags, prompts, processor, shadow, tokenizer, resolver
|
|
8
|
+
from ..core import structure, tags, prompts, processor, shadow, tokenizer, common
|
|
11
9
|
|
|
12
10
|
app = typer.Typer()
|
|
13
11
|
console = Console()
|
|
14
12
|
|
|
15
|
-
def _load_config_filters(root_path: Path):
|
|
16
|
-
config_path = root_path / ".codigest" / "config.toml"
|
|
17
|
-
extensions = None
|
|
18
|
-
exclude_patterns = []
|
|
19
|
-
|
|
20
|
-
if config_path.exists():
|
|
21
|
-
try:
|
|
22
|
-
with open(config_path, "rb") as f:
|
|
23
|
-
data = tomllib.load(f)
|
|
24
|
-
filters = data.get("filter", {})
|
|
25
|
-
ext_list = filters.get("extensions", [])
|
|
26
|
-
if ext_list:
|
|
27
|
-
extensions = set(ext_list)
|
|
28
|
-
exclude_patterns = filters.get("exclude_patterns", [])
|
|
29
|
-
except Exception:
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
return extensions, exclude_patterns
|
|
33
|
-
|
|
34
|
-
def _find_project_root(start_path: Path) -> Path:
|
|
35
|
-
current = start_path.resolve()
|
|
36
|
-
for parent in [current] + list(current.parents):
|
|
37
|
-
if (parent / ".codigest").exists() or (parent / ".git").exists():
|
|
38
|
-
return parent
|
|
39
|
-
return start_path
|
|
40
|
-
|
|
41
13
|
@app.callback(invoke_without_command=True)
|
|
42
14
|
def handle(
|
|
43
15
|
targets: list[Path] = typer.Argument(
|
|
@@ -51,32 +23,23 @@ def handle(
|
|
|
51
23
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
|
|
52
24
|
line_numbers: bool = typer.Option(False, "--lines", "-l", help="Add line numbers to code blocks"),
|
|
53
25
|
yes: bool = typer.Option(False, "-y", "--yes", help="Skip confirmation prompt"),
|
|
54
|
-
# [추가] 의존성 해결 옵션
|
|
55
26
|
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports for local files"),
|
|
56
27
|
):
|
|
57
28
|
"""
|
|
58
29
|
Scans the codebase.
|
|
59
30
|
If TARGETS provided, only scans those paths within the project.
|
|
60
31
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Scope validation
|
|
65
|
-
if scan_scope:
|
|
66
|
-
for p in scan_scope:
|
|
67
|
-
if not p.is_relative_to(root_path):
|
|
68
|
-
# [수정] 이모티콘 제거
|
|
69
|
-
console.print(f"[yellow][Warning] {p.name} is outside project root {root_path.name}[/yellow]")
|
|
32
|
+
# [1] Context Setup (Centralized)
|
|
33
|
+
ctx = common.get_context(targets)
|
|
34
|
+
root_path = ctx.root_path
|
|
70
35
|
|
|
71
36
|
# Init check
|
|
72
37
|
artifact_dir = root_path / ".codigest"
|
|
73
38
|
if not artifact_dir.exists():
|
|
74
|
-
# [수정] 이모티콘 제거
|
|
75
39
|
console.print(f"[yellow][Warning] .codigest directory missing in {root_path.name}. Running init...[/yellow]")
|
|
76
40
|
try:
|
|
77
41
|
artifact_dir.mkdir(exist_ok=True)
|
|
78
42
|
except PermissionError:
|
|
79
|
-
# [수정] 이모티콘 제거
|
|
80
43
|
console.print(f"[red][Error] Cannot create .codigest at {root_path}[/red]")
|
|
81
44
|
raise typer.Exit(1)
|
|
82
45
|
|
|
@@ -84,83 +47,57 @@ def handle(
|
|
|
84
47
|
prompt_engine = prompts.get_engine(root_path)
|
|
85
48
|
anchor = shadow.ContextAnchor(root_path)
|
|
86
49
|
|
|
87
|
-
|
|
88
|
-
if not all:
|
|
89
|
-
extensions, extra_ignores = _load_config_filters(root_path)
|
|
90
|
-
|
|
91
|
-
# ----------------------------------------------------------------
|
|
92
|
-
# 1. Pre-flight Scan (Fast)
|
|
93
|
-
# ----------------------------------------------------------------
|
|
50
|
+
# [2] File Discovery via Context
|
|
94
51
|
with Progress(
|
|
95
52
|
SpinnerColumn(),
|
|
96
|
-
|
|
97
|
-
TextColumn("[bold blue]Scanning file structure...[/bold blue]"),
|
|
53
|
+
TextColumn("[bold blue]Scanning...[/bold blue]"),
|
|
98
54
|
transient=True,
|
|
99
55
|
console=console
|
|
100
56
|
) as progress:
|
|
101
57
|
task = progress.add_task("scanning", total=None)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
58
|
+
|
|
59
|
+
# ★ All logic delegated to common.py
|
|
60
|
+
files = ctx.get_target_files(
|
|
61
|
+
targets=targets,
|
|
62
|
+
ignore_config=all,
|
|
63
|
+
resolve_deps=resolve
|
|
107
64
|
)
|
|
108
65
|
|
|
109
|
-
# [추가] 의존성 해결 로직 (Resolver)
|
|
110
|
-
if resolve:
|
|
111
|
-
progress.update(task, description="[bold blue]Resolving dependencies...[/bold blue]")
|
|
112
|
-
initial_count = len(files)
|
|
113
|
-
files = resolver.resolve_dependencies(root_path, files)
|
|
114
|
-
resolved_count = len(files)
|
|
115
|
-
if resolved_count > initial_count:
|
|
116
|
-
console.print(f"[dim]Resolved {resolved_count - initial_count} dependency files.[/dim]")
|
|
117
|
-
|
|
118
66
|
progress.update(task, completed=100)
|
|
119
67
|
|
|
120
|
-
#
|
|
68
|
+
# [3] Pre-flight Check
|
|
121
69
|
total_files = len(files)
|
|
122
70
|
total_size = sum(f.stat().st_size for f in files)
|
|
123
71
|
est_tokens = int(total_size / 4)
|
|
124
72
|
|
|
125
|
-
# Display Plan
|
|
126
|
-
# [수정] 이모티콘 제거 및 레이아웃 정리
|
|
127
73
|
console.print(Panel(f"""[bold]Scan Plan[/bold]
|
|
128
74
|
Target: [cyan]{root_path}[/cyan]
|
|
129
75
|
Scope: {total_files} files
|
|
130
76
|
Est. Size: {decimal(total_size)}
|
|
131
77
|
Est. Tokens: ~{est_tokens:,}""", expand=False))
|
|
132
78
|
|
|
133
|
-
# --- Smart Confirmation Logic ---
|
|
134
79
|
TOKEN_THRESHOLD = 30000
|
|
135
80
|
FILE_COUNT_THRESHOLD = 100
|
|
136
|
-
|
|
137
81
|
is_large_context = est_tokens > TOKEN_THRESHOLD or total_files > FILE_COUNT_THRESHOLD
|
|
138
82
|
|
|
139
83
|
if yes:
|
|
140
|
-
pass
|
|
84
|
+
pass
|
|
141
85
|
elif is_large_context:
|
|
142
|
-
# [수정] 이모티콘 제거
|
|
143
86
|
console.print(f"[yellow][Warning] Large context detected (> {TOKEN_THRESHOLD:,} tokens or > {FILE_COUNT_THRESHOLD} files).[/yellow]")
|
|
144
|
-
# [수정] 이모티콘 제거
|
|
145
87
|
if not typer.confirm("Proceed with digestion?"):
|
|
146
88
|
console.print("[red]Aborted.[/red]")
|
|
147
89
|
raise typer.Exit()
|
|
148
90
|
else:
|
|
149
|
-
# [수정] 이모티콘 제거
|
|
150
91
|
console.print("[dim]Small context detected. Automatically proceeding...[/dim]")
|
|
151
92
|
|
|
152
|
-
#
|
|
153
|
-
# 2. Execution (Heavy)
|
|
154
|
-
# ----------------------------------------------------------------
|
|
93
|
+
# [4] Execution
|
|
155
94
|
with Progress(
|
|
156
95
|
SpinnerColumn(),
|
|
157
|
-
# [수정] 이모티콘 제거
|
|
158
96
|
TextColumn("[bold blue]Generating Snapshot...[/bold blue]"),
|
|
159
97
|
transient=True,
|
|
160
98
|
console=console
|
|
161
99
|
) as progress:
|
|
162
100
|
|
|
163
|
-
# Diff Report
|
|
164
101
|
if anchor.has_history():
|
|
165
102
|
try:
|
|
166
103
|
diff_content = anchor.get_changes(files)
|
|
@@ -170,16 +107,17 @@ def handle(
|
|
|
170
107
|
except Exception:
|
|
171
108
|
pass
|
|
172
109
|
|
|
173
|
-
# Tree Generation
|
|
174
110
|
tree_str = structure.generate_ascii_tree(files, root_path)
|
|
175
111
|
|
|
176
|
-
# Content Reading & Tagging
|
|
177
112
|
file_blocks = []
|
|
178
113
|
for file_path in files:
|
|
179
|
-
|
|
114
|
+
try:
|
|
115
|
+
rel_path = file_path.relative_to(root_path).as_posix()
|
|
116
|
+
except ValueError:
|
|
117
|
+
rel_path = f"[EXTERNAL]/{file_path.name}"
|
|
118
|
+
|
|
180
119
|
try:
|
|
181
120
|
content = processor.read_file_content(file_path, add_line_numbers=line_numbers)
|
|
182
|
-
|
|
183
121
|
block = tags.file(rel_path, content)
|
|
184
122
|
file_blocks.append(block)
|
|
185
123
|
except Exception:
|
|
@@ -187,7 +125,6 @@ def handle(
|
|
|
187
125
|
|
|
188
126
|
source_code_blob = "\n\n".join(file_blocks)
|
|
189
127
|
|
|
190
|
-
# Template Rendering
|
|
191
128
|
try:
|
|
192
129
|
snapshot_content = prompt_engine.render(
|
|
193
130
|
"snapshot",
|
|
@@ -197,23 +134,18 @@ def handle(
|
|
|
197
134
|
instruction=message
|
|
198
135
|
)
|
|
199
136
|
except Exception as e:
|
|
200
|
-
# [수정] 이모티콘 제거
|
|
201
137
|
console.print(f"[red][Error] Template Rendering Failed:[/red] {e}")
|
|
202
138
|
raise typer.Exit(1)
|
|
203
139
|
|
|
204
|
-
# Final Actions
|
|
205
140
|
try:
|
|
206
141
|
anchor.update(files)
|
|
207
142
|
except Exception as e:
|
|
208
|
-
# [수정] 이모티콘 제거
|
|
209
143
|
console.print(f"[yellow][Warning] Failed to update context anchor: {e}[/yellow]")
|
|
210
144
|
|
|
211
145
|
try:
|
|
212
146
|
output_path.write_text(snapshot_content, encoding="utf-8")
|
|
213
|
-
|
|
214
147
|
final_token_count = tokenizer.estimate_tokens(snapshot_content)
|
|
215
148
|
|
|
216
|
-
# [수정] 이모티콘 제거 및 메시지 단순화
|
|
217
149
|
console.print("[bold green]Snapshot Saved![/bold green]")
|
|
218
150
|
console.print(f" Path: [underline]{output_path}[/underline]")
|
|
219
151
|
console.print(f" Final Tokens: [bold cyan]~{final_token_count:,}[/bold cyan]")
|
|
@@ -221,10 +153,8 @@ def handle(
|
|
|
221
153
|
if anchor.has_history():
|
|
222
154
|
pre_diff_path = artifact_dir / "previous_changes.diff"
|
|
223
155
|
if pre_diff_path.exists() and pre_diff_path.stat().st_size > 0:
|
|
224
|
-
# [수정] 이모티콘 제거
|
|
225
156
|
console.print(f" [dim]Changes before this scan saved to: {pre_diff_path.name}[/dim]")
|
|
226
157
|
|
|
227
158
|
except Exception as e:
|
|
228
|
-
# [수정] 이모티콘 제거
|
|
229
159
|
console.print(f"[bold red][Error] Save Failed:[/bold red] {e}")
|
|
230
160
|
raise typer.Exit(1)
|
|
@@ -4,10 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
6
6
|
|
|
7
|
-
from ..core import
|
|
8
|
-
|
|
9
|
-
# Reuse config loader
|
|
10
|
-
from .scan import _load_config_filters
|
|
7
|
+
from ..core import prompts, shadow, semdiff, tags, tokenizer, common
|
|
11
8
|
|
|
12
9
|
app = typer.Typer()
|
|
13
10
|
console = Console()
|
|
@@ -18,17 +15,22 @@ def handle(
|
|
|
18
15
|
copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
|
|
19
16
|
save: bool = typer.Option(True, help="Save to .codigest/semdiff.xml"),
|
|
20
17
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
|
|
18
|
+
# [추가] resolve 옵션
|
|
19
|
+
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
|
|
21
20
|
):
|
|
22
21
|
"""
|
|
23
22
|
[Advanced] Generates a Semantic Diff (AST-based) report.
|
|
24
23
|
Shows ADDED/REMOVED/MODIFIED functions & classes instead of raw text lines.
|
|
25
24
|
"""
|
|
26
|
-
|
|
25
|
+
# [1] Context Setup
|
|
26
|
+
ctx = common.get_context(target)
|
|
27
|
+
root_path = ctx.root_path
|
|
28
|
+
|
|
27
29
|
anchor = shadow.ContextAnchor(root_path)
|
|
28
30
|
|
|
29
31
|
last_update = anchor.get_last_update_time()
|
|
30
32
|
if last_update == "Never":
|
|
31
|
-
console.print("[yellow]No scan history found. Run [bold]
|
|
33
|
+
console.print("[yellow]⚠️ No scan history found. Run [bold]cdg scan[/bold] first.[/yellow]")
|
|
32
34
|
raise typer.Exit(1)
|
|
33
35
|
|
|
34
36
|
console.print(f"[dim]Analyzing structural changes since ({last_update})...[/dim]")
|
|
@@ -43,10 +45,10 @@ def handle(
|
|
|
43
45
|
) as progress:
|
|
44
46
|
task = progress.add_task("semdiff", total=None)
|
|
45
47
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
current_files = scanner.scan_project(root_path, extensions, extra_ignores)
|
|
48
|
+
# [2] Get Files via Context
|
|
49
|
+
current_files = ctx.get_target_files(resolve_deps=resolve)
|
|
49
50
|
|
|
51
|
+
# Only check files that have text changes first (Optimization)
|
|
50
52
|
changed_files_paths = anchor.get_changed_files(list(current_files))
|
|
51
53
|
|
|
52
54
|
target_files = []
|
|
@@ -54,58 +56,62 @@ def handle(
|
|
|
54
56
|
if p.suffix in (".py", ".pyi"):
|
|
55
57
|
target_files.append(p)
|
|
56
58
|
|
|
57
|
-
#
|
|
59
|
+
# [3] Compare Each File
|
|
58
60
|
for file_path in target_files:
|
|
59
|
-
|
|
61
|
+
# Handle Path String (Support External Files)
|
|
62
|
+
try:
|
|
63
|
+
rel_path = file_path.relative_to(root_path)
|
|
64
|
+
rel_path_str = rel_path.as_posix()
|
|
65
|
+
except ValueError:
|
|
66
|
+
# External files tracked via resolve logic
|
|
67
|
+
rel_path = None # Anchor might not support reading external files yet
|
|
68
|
+
rel_path_str = f"[EXTERNAL]/{file_path.name}"
|
|
60
69
|
|
|
61
|
-
# Read Content
|
|
70
|
+
# Read New Content
|
|
62
71
|
try:
|
|
63
72
|
new_code = file_path.read_text(encoding="utf-8") if file_path.exists() else ""
|
|
64
73
|
except:
|
|
65
74
|
new_code = ""
|
|
66
|
-
|
|
67
|
-
old_code = anchor.read_anchor_file(rel_path)
|
|
68
75
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
76
|
+
# Read Old Content (From Anchor)
|
|
77
|
+
# Note: Anchor only stores files inside root. External files will likely return empty old_code.
|
|
78
|
+
if rel_path:
|
|
79
|
+
old_code = anchor.read_anchor_file(rel_path)
|
|
80
|
+
else:
|
|
81
|
+
old_code = ""
|
|
72
82
|
|
|
73
83
|
changes = semdiff.compare(old_code, new_code)
|
|
74
84
|
|
|
75
85
|
if changes:
|
|
76
86
|
change_lines = []
|
|
77
87
|
for ch in changes:
|
|
78
|
-
if ch.change_type == "ADDED":
|
|
79
|
-
|
|
80
|
-
elif ch.change_type == "
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
icon = "⚠️ [SIGNATURE]" # 중요: 시그니처 변경
|
|
84
|
-
else:
|
|
85
|
-
icon = "✏️ [LOGIC] "
|
|
86
|
-
|
|
88
|
+
if ch.change_type == "ADDED": icon = "➕ [ADDED] "
|
|
89
|
+
elif ch.change_type == "REMOVED": icon = "➖ [REMOVED] "
|
|
90
|
+
elif ch.change_type == "MODIFIED": icon = "⚠️ [SIGNATURE]"
|
|
91
|
+
else: icon = "✏️ [LOGIC] "
|
|
92
|
+
|
|
87
93
|
detail = f" :: {ch.details}" if ch.details else ""
|
|
88
94
|
line = f"{icon} {ch.symbol.type} {ch.symbol.name}{ch.symbol.signature}{detail}"
|
|
89
95
|
change_lines.append(line)
|
|
90
96
|
|
|
91
|
-
# File
|
|
97
|
+
# File Status Tag
|
|
92
98
|
file_status = ""
|
|
93
|
-
if not new_code:
|
|
94
|
-
|
|
95
|
-
elif not old_code:
|
|
96
|
-
file_status = " (NEW)"
|
|
99
|
+
if not new_code: file_status = " (DELETED)"
|
|
100
|
+
elif not old_code: file_status = " (NEW)"
|
|
97
101
|
|
|
98
102
|
raw_body = chr(10).join(change_lines)
|
|
99
|
-
|
|
103
|
+
|
|
104
|
+
# Use unified tags factory
|
|
105
|
+
block = tags.file(rel_path_str, raw_body, status=file_status)
|
|
100
106
|
reports.append(block)
|
|
101
107
|
|
|
102
108
|
progress.update(task, completed=100)
|
|
103
109
|
|
|
104
110
|
if not reports:
|
|
105
|
-
console.print("[green]
|
|
111
|
+
console.print("[green]No structural (AST) changes detected.[/green]")
|
|
106
112
|
return
|
|
107
113
|
|
|
108
|
-
# Render
|
|
114
|
+
# [4] Render
|
|
109
115
|
report_content = "\n".join(reports)
|
|
110
116
|
prompt_engine = prompts.get_engine(root_path)
|
|
111
117
|
|
|
@@ -122,14 +128,14 @@ def handle(
|
|
|
122
128
|
raise typer.Exit(1)
|
|
123
129
|
|
|
124
130
|
token_count = tokenizer.estimate_tokens(final_output)
|
|
125
|
-
console.print(f"[bold green]
|
|
131
|
+
console.print(f"[bold green]SemDiff Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
|
|
126
132
|
|
|
127
133
|
if copy:
|
|
128
134
|
pyperclip.copy(final_output)
|
|
129
|
-
console.print("[dim]
|
|
135
|
+
console.print("[dim]Clipboard copied[/dim]")
|
|
130
136
|
|
|
131
137
|
if save:
|
|
132
138
|
out_path = root_path / ".codigest" / "semdiff.xml"
|
|
133
139
|
out_path.parent.mkdir(exist_ok=True)
|
|
134
140
|
out_path.write_text(final_output, encoding="utf-8")
|
|
135
|
-
console.print(f"[dim]Saved to {out_path}[/dim]")
|
|
141
|
+
console.print(f"[dim]Saved to {out_path}[/dim]")
|