contextcraft-ai 0.2.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.
- contextcraft/__init__.py +44 -0
- contextcraft/__main__.py +8 -0
- contextcraft/ast_parser.py +147 -0
- contextcraft/cli.py +336 -0
- contextcraft/core.py +235 -0
- contextcraft/elider.py +232 -0
- contextcraft/formatter.py +419 -0
- contextcraft/graph.py +495 -0
- contextcraft/languages/__init__.py +48 -0
- contextcraft/languages/base.py +158 -0
- contextcraft/languages/c.py +54 -0
- contextcraft/languages/cpp.py +35 -0
- contextcraft/languages/csharp.py +44 -0
- contextcraft/languages/css.py +79 -0
- contextcraft/languages/data.py +33 -0
- contextcraft/languages/generic.py +119 -0
- contextcraft/languages/go.py +90 -0
- contextcraft/languages/html.py +119 -0
- contextcraft/languages/java.py +43 -0
- contextcraft/languages/json.py +71 -0
- contextcraft/languages/python.py +93 -0
- contextcraft/languages/rust.py +83 -0
- contextcraft/languages/toml.py +64 -0
- contextcraft/languages/typescript.py +110 -0
- contextcraft/languages/yaml.py +73 -0
- contextcraft/models.py +410 -0
- contextcraft/optimizer.py +335 -0
- contextcraft/sanitizer.py +313 -0
- contextcraft/scanner.py +454 -0
- contextcraft_ai-0.2.0.dist-info/METADATA +199 -0
- contextcraft_ai-0.2.0.dist-info/RECORD +34 -0
- contextcraft_ai-0.2.0.dist-info/WHEEL +4 -0
- contextcraft_ai-0.2.0.dist-info/entry_points.txt +3 -0
- contextcraft_ai-0.2.0.dist-info/licenses/LICENSE +21 -0
contextcraft/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""ContextCraft: AST-Aware, Dependency-Graph-Ranked, Token-Budgeted Codebase Compressor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextcraft.core import ContextCraft
|
|
6
|
+
from contextcraft.models import (
|
|
7
|
+
ASTParseError,
|
|
8
|
+
ByteRange,
|
|
9
|
+
CompilationConfig,
|
|
10
|
+
CompilationError,
|
|
11
|
+
CompilationResult,
|
|
12
|
+
CompilationStats,
|
|
13
|
+
ContextCraftError,
|
|
14
|
+
FidelityLevel,
|
|
15
|
+
FileAnalysis,
|
|
16
|
+
InvalidBudgetError,
|
|
17
|
+
OutputFormat,
|
|
18
|
+
RepositoryNotFoundError,
|
|
19
|
+
SecretKind,
|
|
20
|
+
SecretMatch,
|
|
21
|
+
SymbolNode,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.2.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"ASTParseError",
|
|
28
|
+
"ByteRange",
|
|
29
|
+
"CompilationConfig",
|
|
30
|
+
"CompilationError",
|
|
31
|
+
"CompilationResult",
|
|
32
|
+
"CompilationStats",
|
|
33
|
+
"ContextCraft",
|
|
34
|
+
"ContextCraftError",
|
|
35
|
+
"FidelityLevel",
|
|
36
|
+
"FileAnalysis",
|
|
37
|
+
"InvalidBudgetError",
|
|
38
|
+
"OutputFormat",
|
|
39
|
+
"RepositoryNotFoundError",
|
|
40
|
+
"SecretKind",
|
|
41
|
+
"SecretMatch",
|
|
42
|
+
"SymbolNode",
|
|
43
|
+
"__version__",
|
|
44
|
+
]
|
contextcraft/__main__.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Shared Tree-sitter traversal and language-specific extraction dispatch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from contextcraft.languages import get_extractor
|
|
8
|
+
from contextcraft.models import ASTParseResult, SymbolNode
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from tree_sitter import Node
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from tree_sitter_language_pack import get_parser
|
|
15
|
+
|
|
16
|
+
HAS_TREE_SITTER = True
|
|
17
|
+
except ImportError: # pragma: no cover
|
|
18
|
+
HAS_TREE_SITTER = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UniversalASTParser:
|
|
22
|
+
"""Extracts architectural symbols, byte spans, and dependency references."""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def parse(cls, content: str, language_id: str) -> tuple[list[SymbolNode], list[str]]:
|
|
26
|
+
"""Parse source code into `(symbols, imports)`."""
|
|
27
|
+
detailed = cls.parse_detailed(content, language_id)
|
|
28
|
+
return detailed.symbols, detailed.imports
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse_detailed(cls, content: str, language_id: str) -> ASTParseResult:
|
|
32
|
+
"""Parse symbols, imports, definitions, and identifier references."""
|
|
33
|
+
if not content.strip():
|
|
34
|
+
return ASTParseResult([], [], [], [])
|
|
35
|
+
|
|
36
|
+
extractor = get_extractor(language_id)
|
|
37
|
+
if not HAS_TREE_SITTER:
|
|
38
|
+
return extractor.parse_fallback(content)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
parser = get_parser(language_id)
|
|
42
|
+
except Exception:
|
|
43
|
+
# Grammar loading can fail for unsupported, missing, or uncached grammars.
|
|
44
|
+
return extractor.parse_fallback(content)
|
|
45
|
+
|
|
46
|
+
source = content.encode("utf-8")
|
|
47
|
+
tree = parser.parse(source)
|
|
48
|
+
if not extractor.should_extract(tree.root_node):
|
|
49
|
+
return extractor.parse_fallback(content)
|
|
50
|
+
return cls._extract_from_tree(tree.root_node, source, language_id)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def _extract_from_tree(
|
|
54
|
+
cls,
|
|
55
|
+
root: Node,
|
|
56
|
+
raw_bytes: bytes,
|
|
57
|
+
language_id: str,
|
|
58
|
+
) -> ASTParseResult:
|
|
59
|
+
extractor = get_extractor(language_id)
|
|
60
|
+
symbols: list[SymbolNode] = []
|
|
61
|
+
imports: dict[str, None] = {}
|
|
62
|
+
definitions: dict[str, None] = {}
|
|
63
|
+
references: dict[str, None] = {}
|
|
64
|
+
|
|
65
|
+
def traverse(
|
|
66
|
+
node: Node,
|
|
67
|
+
parent_name: str | None = None,
|
|
68
|
+
parent_type: str | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
scope = extractor.enclosing_scope(node, raw_bytes)
|
|
71
|
+
if scope is not None:
|
|
72
|
+
scope_name, parent_type = scope
|
|
73
|
+
parent_name = f"{parent_name}.{scope_name}" if parent_name else scope_name
|
|
74
|
+
|
|
75
|
+
declaration, decorators = extractor.unwrap_declaration(node, raw_bytes)
|
|
76
|
+
if node.type in extractor.IMPORT_NODE_TYPES:
|
|
77
|
+
text = cls._node_text(node, raw_bytes).strip()
|
|
78
|
+
if text:
|
|
79
|
+
imports[text] = None
|
|
80
|
+
|
|
81
|
+
kind = extractor.symbol_kind(declaration, parent_type)
|
|
82
|
+
if kind is not None:
|
|
83
|
+
record_symbol(declaration, node, kind, parent_name, parent_type, decorators)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if extractor.is_reference(node):
|
|
87
|
+
name = cls._node_text(node, raw_bytes).strip()
|
|
88
|
+
if len(name) >= 3:
|
|
89
|
+
references[name] = None
|
|
90
|
+
|
|
91
|
+
for child in node.children:
|
|
92
|
+
traverse(child, parent_name, parent_type)
|
|
93
|
+
|
|
94
|
+
def record_symbol(
|
|
95
|
+
node: Node,
|
|
96
|
+
outer_node: Node,
|
|
97
|
+
kind: str,
|
|
98
|
+
parent_name: str | None,
|
|
99
|
+
parent_type: str | None,
|
|
100
|
+
decorators: list[str],
|
|
101
|
+
) -> None:
|
|
102
|
+
if extractor.name_node(node) is None:
|
|
103
|
+
for child in node.children:
|
|
104
|
+
traverse(child, parent_name, parent_type)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
name = extractor.symbol_name(node, raw_bytes)
|
|
108
|
+
if not name:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
qualified_name = f"{parent_name}.{name}" if parent_name else name
|
|
112
|
+
definitions[name] = None
|
|
113
|
+
if parent_name:
|
|
114
|
+
definitions[qualified_name] = None
|
|
115
|
+
|
|
116
|
+
docstring, doc_range = extractor.extract_docstring(node, outer_node, raw_bytes)
|
|
117
|
+
body_range = extractor.elidable_body_range(
|
|
118
|
+
node, raw_bytes, doc_range.end_byte if doc_range is not None else None
|
|
119
|
+
)
|
|
120
|
+
symbols.append(
|
|
121
|
+
SymbolNode(
|
|
122
|
+
name=name,
|
|
123
|
+
qualified_name=qualified_name,
|
|
124
|
+
kind=kind,
|
|
125
|
+
signature=extractor.extract_signature(node, outer_node, raw_bytes),
|
|
126
|
+
docstring=docstring,
|
|
127
|
+
full_range=extractor.node_range(outer_node),
|
|
128
|
+
body_range=body_range,
|
|
129
|
+
is_exported=extractor.is_exported(node, name, raw_bytes),
|
|
130
|
+
parent_symbol=parent_name,
|
|
131
|
+
decorators=decorators,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
for child in node.children:
|
|
135
|
+
traverse(child, qualified_name, node.type)
|
|
136
|
+
|
|
137
|
+
traverse(root)
|
|
138
|
+
return ASTParseResult(
|
|
139
|
+
symbols=symbols,
|
|
140
|
+
imports=list(imports),
|
|
141
|
+
defined_identifiers=list(definitions),
|
|
142
|
+
referenced_identifiers=list(references),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _node_text(node: Node, raw_bytes: bytes) -> str:
|
|
147
|
+
return raw_bytes[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
contextcraft/cli.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Rich CLI interface for ContextCraft.
|
|
2
|
+
|
|
3
|
+
Provides a fast, zero-config command-line interface:
|
|
4
|
+
contextcraft . --budget 32k --focus "authentication flow" -o context.md
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich import box
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from contextcraft import __version__
|
|
22
|
+
from contextcraft.core import ContextCraft
|
|
23
|
+
from contextcraft.models import (
|
|
24
|
+
CompilationResult,
|
|
25
|
+
ContextCraftError,
|
|
26
|
+
FidelityLevel,
|
|
27
|
+
OutputFormat,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(
|
|
31
|
+
name="contextcraft",
|
|
32
|
+
help=(
|
|
33
|
+
"AST-Aware, Dependency-Graph-Ranked, Token-Budgeted "
|
|
34
|
+
"Semantic Codebase Compressor for LLMs."
|
|
35
|
+
),
|
|
36
|
+
add_completion=False,
|
|
37
|
+
no_args_is_help=False,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
err_console = Console(stderr=True)
|
|
41
|
+
out_console = Console(stderr=False)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _version_callback(value: bool) -> None:
|
|
45
|
+
if value:
|
|
46
|
+
out_console.print(f"contextcraft {__version__}")
|
|
47
|
+
raise typer.Exit(code=0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def main(
|
|
52
|
+
path: Annotated[
|
|
53
|
+
Path,
|
|
54
|
+
typer.Argument(
|
|
55
|
+
help="Path to the target Git repository or source directory.",
|
|
56
|
+
show_default=True,
|
|
57
|
+
),
|
|
58
|
+
] = Path("."),
|
|
59
|
+
budget: Annotated[
|
|
60
|
+
str,
|
|
61
|
+
typer.Option(
|
|
62
|
+
"--budget",
|
|
63
|
+
"-b",
|
|
64
|
+
help="Strict token budget (supports shorthand like '16k', '32k', '128k', or '8000').",
|
|
65
|
+
show_default=True,
|
|
66
|
+
),
|
|
67
|
+
] = "32k",
|
|
68
|
+
focus: Annotated[
|
|
69
|
+
str | None,
|
|
70
|
+
typer.Option(
|
|
71
|
+
"--focus",
|
|
72
|
+
"-f",
|
|
73
|
+
help="Natural-language query or symbol keywords to bias Personalized PageRank.",
|
|
74
|
+
),
|
|
75
|
+
] = None,
|
|
76
|
+
focus_file: Annotated[
|
|
77
|
+
list[str] | None,
|
|
78
|
+
typer.Option(
|
|
79
|
+
"--focus-file",
|
|
80
|
+
help="Specific relative file path or glob to boost in PageRank (repeatable).",
|
|
81
|
+
),
|
|
82
|
+
] = None,
|
|
83
|
+
output_format: Annotated[
|
|
84
|
+
OutputFormat,
|
|
85
|
+
typer.Option(
|
|
86
|
+
"--format",
|
|
87
|
+
"-m",
|
|
88
|
+
case_sensitive=False,
|
|
89
|
+
help="Target output format: markdown, xml (Claude-optimized), or json.",
|
|
90
|
+
show_default=True,
|
|
91
|
+
),
|
|
92
|
+
] = OutputFormat.MARKDOWN,
|
|
93
|
+
output: Annotated[
|
|
94
|
+
Path | None,
|
|
95
|
+
typer.Option(
|
|
96
|
+
"--output",
|
|
97
|
+
"-o",
|
|
98
|
+
help="Write compiled context to this file path instead of stdout.",
|
|
99
|
+
),
|
|
100
|
+
] = None,
|
|
101
|
+
include: Annotated[
|
|
102
|
+
list[str] | None,
|
|
103
|
+
typer.Option(
|
|
104
|
+
"--include",
|
|
105
|
+
"-i",
|
|
106
|
+
help="Glob pattern of files to include (repeatable).",
|
|
107
|
+
),
|
|
108
|
+
] = None,
|
|
109
|
+
exclude: Annotated[
|
|
110
|
+
list[str] | None,
|
|
111
|
+
typer.Option(
|
|
112
|
+
"--exclude",
|
|
113
|
+
"-e",
|
|
114
|
+
help="Additional glob pattern of files to exclude (repeatable).",
|
|
115
|
+
),
|
|
116
|
+
] = None,
|
|
117
|
+
copy: Annotated[
|
|
118
|
+
bool,
|
|
119
|
+
typer.Option(
|
|
120
|
+
"--copy",
|
|
121
|
+
"-c",
|
|
122
|
+
help="Copy the compiled output directly to the system clipboard.",
|
|
123
|
+
),
|
|
124
|
+
] = False,
|
|
125
|
+
no_redact: Annotated[
|
|
126
|
+
bool,
|
|
127
|
+
typer.Option(
|
|
128
|
+
"--no-redact",
|
|
129
|
+
help="Disable automatic secret and API key redaction.",
|
|
130
|
+
),
|
|
131
|
+
] = False,
|
|
132
|
+
no_gitignore: Annotated[
|
|
133
|
+
bool,
|
|
134
|
+
typer.Option(
|
|
135
|
+
"--no-gitignore",
|
|
136
|
+
help="Do not respect .gitignore and .contextignore files.",
|
|
137
|
+
),
|
|
138
|
+
] = False,
|
|
139
|
+
quiet: Annotated[
|
|
140
|
+
bool,
|
|
141
|
+
typer.Option(
|
|
142
|
+
"--quiet",
|
|
143
|
+
"-q",
|
|
144
|
+
help="Suppress the Rich telemetry summary table on stderr.",
|
|
145
|
+
),
|
|
146
|
+
] = False,
|
|
147
|
+
version: Annotated[
|
|
148
|
+
bool,
|
|
149
|
+
typer.Option(
|
|
150
|
+
"--version",
|
|
151
|
+
"-V",
|
|
152
|
+
callback=_version_callback,
|
|
153
|
+
is_eager=True,
|
|
154
|
+
help="Show ContextCraft version and exit.",
|
|
155
|
+
),
|
|
156
|
+
] = False,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Compile a repository into an optimal, token-budgeted LLM context artifact."""
|
|
159
|
+
_ = version
|
|
160
|
+
try:
|
|
161
|
+
exclude_list = list(exclude) if exclude else []
|
|
162
|
+
if output is not None:
|
|
163
|
+
# Ensure we never accidentally ingest the output file being written
|
|
164
|
+
exclude_list.append(output.name)
|
|
165
|
+
|
|
166
|
+
compiler = ContextCraft(
|
|
167
|
+
budget_tokens=budget,
|
|
168
|
+
focus_query=focus,
|
|
169
|
+
focus_files=list(focus_file) if focus_file else [],
|
|
170
|
+
output_format=output_format,
|
|
171
|
+
include_patterns=list(include) if include else [],
|
|
172
|
+
exclude_patterns=exclude_list,
|
|
173
|
+
respect_gitignore=not no_gitignore,
|
|
174
|
+
redact_secrets=not no_redact,
|
|
175
|
+
)
|
|
176
|
+
result = compiler.compile(path)
|
|
177
|
+
except ContextCraftError as exc:
|
|
178
|
+
err_console.print(f"[bold red]ContextCraft Error:[/bold red] {exc}")
|
|
179
|
+
raise typer.Exit(code=1) from exc
|
|
180
|
+
except Exception as exc: # pragma: no cover
|
|
181
|
+
err_console.print(f"[bold red]Unexpected Failure:[/bold red] {exc}")
|
|
182
|
+
raise typer.Exit(code=1) from exc
|
|
183
|
+
|
|
184
|
+
# 1. Emit artifact to file or stdout
|
|
185
|
+
if output is not None:
|
|
186
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
output.write_text(result.content, encoding="utf-8")
|
|
188
|
+
else:
|
|
189
|
+
sys.stdout.write(result.content)
|
|
190
|
+
if not result.content.endswith("\n"):
|
|
191
|
+
sys.stdout.write("\n")
|
|
192
|
+
sys.stdout.flush()
|
|
193
|
+
|
|
194
|
+
# 2. Optionally copy to clipboard
|
|
195
|
+
copied_ok = False
|
|
196
|
+
if copy:
|
|
197
|
+
copied_ok = _copy_to_clipboard(result.content)
|
|
198
|
+
|
|
199
|
+
# 3. Render telemetry dashboard to stderr unless --quiet is set
|
|
200
|
+
if not quiet:
|
|
201
|
+
_render_telemetry_summary(
|
|
202
|
+
result=result,
|
|
203
|
+
output_path=output,
|
|
204
|
+
copied_to_clipboard=copied_ok,
|
|
205
|
+
copy_requested=copy,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _render_telemetry_summary(
|
|
210
|
+
*,
|
|
211
|
+
result: CompilationResult,
|
|
212
|
+
output_path: Path | None,
|
|
213
|
+
copied_to_clipboard: bool,
|
|
214
|
+
copy_requested: bool,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Render a compact Rich telemetry table to `stderr`."""
|
|
217
|
+
stats = result.stats
|
|
218
|
+
|
|
219
|
+
metrics_table = Table(
|
|
220
|
+
box=box.ROUNDED,
|
|
221
|
+
show_header=True,
|
|
222
|
+
header_style="bold cyan",
|
|
223
|
+
expand=False,
|
|
224
|
+
)
|
|
225
|
+
metrics_table.add_column("Metric", style="bold white")
|
|
226
|
+
metrics_table.add_column("Value", justify="right", style="green")
|
|
227
|
+
|
|
228
|
+
metrics_table.add_row("Repository Root", result.root_path)
|
|
229
|
+
metrics_table.add_row(
|
|
230
|
+
"Files Compiled",
|
|
231
|
+
f"{stats.files_compiled} (skipped {stats.ignored_files_skipped} ignored, {stats.binary_files_skipped} binary)",
|
|
232
|
+
)
|
|
233
|
+
metrics_table.add_row("Raw Repository Tokens", f"{stats.raw_tokens:,}")
|
|
234
|
+
metrics_table.add_row(
|
|
235
|
+
"Compressed Tokens",
|
|
236
|
+
f"{stats.compressed_tokens:,} / {stats.budget_tokens:,} ({stats.budget_utilization_pct:.1f}%)",
|
|
237
|
+
)
|
|
238
|
+
metrics_table.add_row(
|
|
239
|
+
"Compression Ratio",
|
|
240
|
+
f"{stats.compression_ratio:.2f}x ({stats.token_savings_pct:.1f}% saved)",
|
|
241
|
+
)
|
|
242
|
+
metrics_table.add_row(
|
|
243
|
+
"Fidelity Allocation",
|
|
244
|
+
(
|
|
245
|
+
f"FULL: {stats.fidelity_counts.get('FULL', 0)} | "
|
|
246
|
+
f"SKEL: {stats.fidelity_counts.get('SKEL', 0)} | "
|
|
247
|
+
f"SIGS: {stats.fidelity_counts.get('SIGS', 0)} | "
|
|
248
|
+
f"TREE: {stats.fidelity_counts.get('TREE', 0)}"
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
metrics_table.add_row(
|
|
252
|
+
"Signature Coverage",
|
|
253
|
+
f"{stats.preserved_signatures_pct:.1f}% ({stats.total_symbols_extracted} AST symbols)",
|
|
254
|
+
)
|
|
255
|
+
if stats.secrets_redacted > 0:
|
|
256
|
+
metrics_table.add_row(
|
|
257
|
+
"Secrets Scrubbed",
|
|
258
|
+
f"[bold yellow]{stats.secrets_redacted} redacted[/bold yellow]",
|
|
259
|
+
)
|
|
260
|
+
metrics_table.add_row("Execution Time", f"{stats.execution_time_ms:.1f} ms")
|
|
261
|
+
|
|
262
|
+
if output_path is not None:
|
|
263
|
+
metrics_table.add_row("Saved Output", str(output_path))
|
|
264
|
+
if copy_requested:
|
|
265
|
+
status_str = "Copied to clipboard" if copied_to_clipboard else "Clipboard unavailable"
|
|
266
|
+
metrics_table.add_row("Clipboard", status_str)
|
|
267
|
+
|
|
268
|
+
err_console.print()
|
|
269
|
+
err_console.print(
|
|
270
|
+
Panel.fit(
|
|
271
|
+
metrics_table,
|
|
272
|
+
title="[bold magenta]ContextCraft Compilation Summary[/bold magenta]",
|
|
273
|
+
border_style="bright_blue",
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Display top-ranked files table if files were compiled
|
|
278
|
+
if result.files:
|
|
279
|
+
top_table = Table(
|
|
280
|
+
title="Top Architectural Files by Personalized PageRank",
|
|
281
|
+
box=box.SIMPLE_HEAD,
|
|
282
|
+
header_style="bold blue",
|
|
283
|
+
)
|
|
284
|
+
top_table.add_column("File Path", style="white")
|
|
285
|
+
top_table.add_column("Fidelity", justify="center")
|
|
286
|
+
top_table.add_column("PageRank", justify="right", style="cyan")
|
|
287
|
+
top_table.add_column("Tokens (Selected / Raw)", justify="right", style="green")
|
|
288
|
+
|
|
289
|
+
badge_styles = {
|
|
290
|
+
FidelityLevel.FULL_SOURCE: "[bold green]FULL[/bold green]",
|
|
291
|
+
FidelityLevel.SKELETON_WITH_DOCSTRINGS: "[bold cyan]SKEL[/bold cyan]",
|
|
292
|
+
FidelityLevel.SIGNATURES_ONLY: "[bold yellow]SIGS[/bold yellow]",
|
|
293
|
+
FidelityLevel.TREE_ONLY: "[dim]TREE[/dim]",
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
ranked = sorted(result.files, key=lambda f: (-f.pagerank_score, f.rel_path))
|
|
297
|
+
for file_obj in ranked[:5]:
|
|
298
|
+
top_table.add_row(
|
|
299
|
+
file_obj.rel_path,
|
|
300
|
+
badge_styles[file_obj.selected_fidelity],
|
|
301
|
+
f"{file_obj.pagerank_score * 100.0:.1f}%",
|
|
302
|
+
f"{file_obj.selected_tokens:,} / {file_obj.raw_tokens:,}",
|
|
303
|
+
)
|
|
304
|
+
err_console.print(top_table)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _copy_to_clipboard(text: str) -> bool:
|
|
308
|
+
"""Copy `text` to the system clipboard using native OS utilities."""
|
|
309
|
+
commands: list[list[str]] = [
|
|
310
|
+
["pbcopy"],
|
|
311
|
+
["wl-copy"],
|
|
312
|
+
["xclip", "-selection", "clipboard"],
|
|
313
|
+
["xsel", "--clipboard", "--input"],
|
|
314
|
+
["clip"],
|
|
315
|
+
]
|
|
316
|
+
for cmd in commands:
|
|
317
|
+
executable = shutil.which(cmd[0])
|
|
318
|
+
if executable is None:
|
|
319
|
+
continue
|
|
320
|
+
try:
|
|
321
|
+
subprocess.run(
|
|
322
|
+
[executable, *cmd[1:]],
|
|
323
|
+
input=text.encode("utf-8"),
|
|
324
|
+
check=True,
|
|
325
|
+
stdout=subprocess.DEVNULL,
|
|
326
|
+
stderr=subprocess.DEVNULL,
|
|
327
|
+
timeout=3,
|
|
328
|
+
)
|
|
329
|
+
return True
|
|
330
|
+
except Exception:
|
|
331
|
+
continue
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__": # pragma: no cover
|
|
336
|
+
app()
|