code-context-control 2.121.0__py3-none-any.whl → 2.123.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.
- cli/c3.py +4 -4
- cli/commands/common.py +1 -1
- cli/commands/parser.py +2 -1
- cli/guide/index.html +1 -1
- cli/guide/workflow.html +1 -1
- cli/hook_pretool_enforce.py +1 -1
- cli/tools/compress.py +119 -215
- cli/tools/read.py +16 -0
- {code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/METADATA +1 -1
- {code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/RECORD +18 -17
- services/claude_md.py +4 -4
- services/codex_integration.py +1 -1
- services/compressor.py +69 -124
- services/dir_map.py +176 -0
- {code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
|
@@ -92,7 +92,7 @@ console = Console() if HAS_RICH else None
|
|
|
92
92
|
# Config
|
|
93
93
|
CONFIG_DIR = ".c3"
|
|
94
94
|
CONFIG_FILE = ".c3/config.json"
|
|
95
|
-
__version__ = "2.
|
|
95
|
+
__version__ = "2.123.0"
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _compress_file_cli(compressor, path, mode="smart", **kw):
|
|
@@ -271,7 +271,7 @@ _STALE_MCP_TOOLS = {
|
|
|
271
271
|
"mcp__c3__c3_remember", # → c3_memory(action='add')
|
|
272
272
|
"mcp__c3__c3_recall", # → c3_memory(action='recall')
|
|
273
273
|
"mcp__c3__c3_sltm_add", # → c3_memory(action='add')
|
|
274
|
-
"mcp__c3__c3_file_map", # →
|
|
274
|
+
"mcp__c3__c3_file_map", # → c3_read(file_path) map
|
|
275
275
|
"mcp__c3__c3_extract", # → c3_read
|
|
276
276
|
"mcp__c3__c3_transcript_search", # → c3_search(action='transcript')
|
|
277
277
|
"mcp__c3__c3_session_log", # → c3_session(action='log')
|
|
@@ -3971,7 +3971,7 @@ def cmd_benchmark(args):
|
|
|
3971
3971
|
"prompt_budget_multiplier": round(_prompt_gain(comp_comp, comp_orig), 2),
|
|
3972
3972
|
},
|
|
3973
3973
|
"file_navigation": {
|
|
3974
|
-
"description": "Use
|
|
3974
|
+
"description": "Use the c3_read file map to choose targeted reads instead of opening whole files blindly.",
|
|
3975
3975
|
"performance_metric": "map success rate",
|
|
3976
3976
|
"with_c3": {"tool": "c3_compress(mode='map')", "total_tokens": file_map_comp, "avg_latency_ms": round(_avg(file_map_c3_latencies), 2), "performance": round((file_map_successes / len(file_map_sample) * 100), 1) if file_map_sample else 0.0},
|
|
3977
3977
|
"without_c3": {"approach": "read full files into context", "total_tokens": file_map_orig, "avg_latency_ms": round(_avg(file_map_baseline_latencies), 2), "performance": 100.0},
|
|
@@ -5005,7 +5005,7 @@ back to native tools as the task progresses.
|
|
|
5005
5005
|
|
|
5006
5006
|
## Quick Reference
|
|
5007
5007
|
- **Search**: `c3_search(query=..., action='code|files|semantic')` — before Grep/Glob
|
|
5008
|
-
- **Read**: `
|
|
5008
|
+
- **Read**: `c3_read(file_path)` maps a file (or directory), then `c3_read(symbols=['Class.method'])` — before native Read
|
|
5009
5009
|
- **Impact**: `c3_impact(target='symbol')` — blast-radius check before editing shared symbols
|
|
5010
5010
|
- **Edit**: `c3_edit(file_path=..., old_string=..., new_string=...)` — before Edit/Write
|
|
5011
5011
|
- **Validate**: `c3_validate(file_path=...)` — after every edit (pyright/tsc type check if installed)
|
cli/commands/common.py
CHANGED
|
@@ -72,7 +72,7 @@ def cmd_index(args, deps: CommandDeps):
|
|
|
72
72
|
def cmd_compress(args, deps: CommandDeps):
|
|
73
73
|
"""Compress a file and show results."""
|
|
74
74
|
config = deps.load_config()
|
|
75
|
-
mode = args.mode or "
|
|
75
|
+
mode = args.mode or "map"
|
|
76
76
|
|
|
77
77
|
compressor = deps.CodeCompressor(str(Path(config.get("project_path", ".")) / ".c3/cache"))
|
|
78
78
|
result = compressor.compress_file(args.file, mode)
|
cli/commands/parser.py
CHANGED
|
@@ -91,7 +91,8 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
91
91
|
|
|
92
92
|
p_compress = subparsers.add_parser("compress", help="Compress a file")
|
|
93
93
|
p_compress.add_argument("file", help="File to compress")
|
|
94
|
-
p_compress.add_argument("--mode", choices=["map", "
|
|
94
|
+
p_compress.add_argument("--mode", choices=["map", "smart", "structure", "outline"], default="map",
|
|
95
|
+
help="map (default) is the canonical file map; the others are signature summaries")
|
|
95
96
|
p_compress.add_argument("--output", "-o", action="store_true", help="Show compressed output")
|
|
96
97
|
|
|
97
98
|
p_context = subparsers.add_parser("context", help="Get relevant context for a query")
|
cli/guide/index.html
CHANGED
|
@@ -636,7 +636,7 @@
|
|
|
636
636
|
<span>Discover files / content — never start with Grep/Glob</span>
|
|
637
637
|
</div>
|
|
638
638
|
<div class="quick-item">
|
|
639
|
-
<code>3.
|
|
639
|
+
<code>3. c3_read(file_path) + c3_read(symbols=…)</code>
|
|
640
640
|
<span>Map then surgically read — never start with native Read</span>
|
|
641
641
|
</div>
|
|
642
642
|
<div class="quick-item">
|
cli/guide/workflow.html
CHANGED
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
<tr>
|
|
316
316
|
<td><span class="badge badge-blue">3</span></td>
|
|
317
317
|
<td><strong>Map + Read</strong></td>
|
|
318
|
-
<td><code>
|
|
318
|
+
<td><code>c3_read(file_path)</code> (map) → <code>c3_read(symbols=…)</code></td>
|
|
319
319
|
<td>Before reading any file content</td>
|
|
320
320
|
</tr>
|
|
321
321
|
<tr>
|
cli/hook_pretool_enforce.py
CHANGED
|
@@ -130,7 +130,7 @@ _VAULT_FILES = frozenset({"config.json", "secrets.enc", "cred_state.json",
|
|
|
130
130
|
# Redirect messages per native tool
|
|
131
131
|
_REDIRECTS = {
|
|
132
132
|
"Read": (
|
|
133
|
-
"Use
|
|
133
|
+
"Use c3_read(file_path='...') to map the file first (one line per symbol), "
|
|
134
134
|
"then c3_read(file_path='...', symbols=['...']) for surgical extraction."
|
|
135
135
|
),
|
|
136
136
|
"Grep": (
|
cli/tools/compress.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""c3_compress — the canonical file map (services/file_map.render_map), one file or a batch.
|
|
2
2
|
|
|
3
|
-
mode
|
|
4
|
-
modes (dense_map, smart, diff, bug_scan,
|
|
3
|
+
There is one mode, `map`, and it is the same text c3_read(file_path) serves.
|
|
4
|
+
The modes that existed before 2.122.0 (dense_map, smart, diff, bug_scan,
|
|
5
|
+
ast, structure, outline) were called twice in 3,932 tool calls; a request
|
|
6
|
+
for one of them is answered with the map plus a one-line notice so an old
|
|
7
|
+
prompt keeps working while it says so (docs/file-map.md § Retired modes).
|
|
8
|
+
"""
|
|
5
9
|
|
|
6
|
-
import json
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
10
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
@@ -15,6 +15,24 @@ from cli.tools._helpers import finalize_with_tokens, show_token_ratios
|
|
|
15
15
|
from core import count_tokens
|
|
16
16
|
from services import access_guard
|
|
17
17
|
|
|
18
|
+
#: Every mode name c3_compress ever accepted, and what to say about it now.
|
|
19
|
+
RETIRED_MODES = {
|
|
20
|
+
"dense_map": "the map is dense now",
|
|
21
|
+
"smart": "the map carries full signatures",
|
|
22
|
+
"structure": "the map carries full signatures",
|
|
23
|
+
"outline": "the map carries full signatures",
|
|
24
|
+
"diff": "use `git diff` or the edit ledger (c3_edits)",
|
|
25
|
+
"bug_scan": "use c3_search(action='exact', query='except ') for handler hotspots",
|
|
26
|
+
"ast": "project architecture: codebase-memory-mcp `cli get_architecture`, or c3_status",
|
|
27
|
+
}
|
|
28
|
+
RETIRED_IN = "2.122.0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def deprecation_line(requested: str) -> str:
|
|
32
|
+
hint = RETIRED_MODES.get(requested, "")
|
|
33
|
+
return (f"[compress:deprecated] mode '{requested}' retired in {RETIRED_IN} — "
|
|
34
|
+
f"map is the only mode ({hint}); see docs/file-map.md")
|
|
35
|
+
|
|
18
36
|
|
|
19
37
|
def map_detail(svc, rel: str, requested_mode: str, *, cache_hit: bool,
|
|
20
38
|
symbols: int = 0, fallback: str = "") -> dict:
|
|
@@ -24,6 +42,7 @@ def map_detail(svc, rel: str, requested_mode: str, *, cache_hit: bool,
|
|
|
24
42
|
(tree_sitter | regex | generic, "" for a pre-2.120.0 record), cache_hit
|
|
25
43
|
whether the record was already fresh. `symbols` is how many the caller
|
|
26
44
|
asked for and `fallback` why a read served a map instead of source.
|
|
45
|
+
`deprecated_mode` (2.122.0) is set when a retired mode name was asked for.
|
|
27
46
|
Flat so telemetry.aggregate_tool_telemetry can fold it without a schema.
|
|
28
47
|
"""
|
|
29
48
|
record = None
|
|
@@ -44,225 +63,117 @@ def map_detail(svc, rel: str, requested_mode: str, *, cache_hit: bool,
|
|
|
44
63
|
detail["symbols"] = int(symbols)
|
|
45
64
|
if fallback:
|
|
46
65
|
detail["fallback"] = fallback
|
|
66
|
+
if requested_mode not in ("map", "read"):
|
|
67
|
+
detail["deprecated_mode"] = requested_mode
|
|
47
68
|
return detail
|
|
48
69
|
|
|
49
70
|
|
|
50
|
-
def compressor_detail(res: dict, requested_mode: str) -> dict:
|
|
51
|
-
"""Telemetry `detail` for a CodeCompressor result (smart/diff/bug_scan…)."""
|
|
52
|
-
backend = "large_fast_path" if res.get("fast_path") else "compressor"
|
|
53
|
-
return {
|
|
54
|
-
"requested_mode": requested_mode,
|
|
55
|
-
"backend": backend,
|
|
56
|
-
"actual_mode": str(res.get("mode") or ""),
|
|
57
|
-
"cache_hit": bool(res.get("cache_hit")),
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _run_memory_mcp_cli(args: list, cwd: str, timeout: int = 30) -> tuple:
|
|
62
|
-
"""Run codebase-memory-mcp CLI and return (success, output_or_error)."""
|
|
63
|
-
binary = shutil.which("codebase-memory-mcp")
|
|
64
|
-
if not binary:
|
|
65
|
-
return False, "not_installed"
|
|
66
|
-
kwargs: dict = {}
|
|
67
|
-
if sys.platform == "win32":
|
|
68
|
-
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
69
|
-
try:
|
|
70
|
-
result = subprocess.run(
|
|
71
|
-
[binary] + args,
|
|
72
|
-
capture_output=True, text=True, timeout=timeout,
|
|
73
|
-
stdin=subprocess.DEVNULL, cwd=cwd, **kwargs,
|
|
74
|
-
)
|
|
75
|
-
if result.returncode == 0 and result.stdout.strip():
|
|
76
|
-
return True, result.stdout.strip()
|
|
77
|
-
return False, result.stderr.strip() or f"exit {result.returncode}"
|
|
78
|
-
except subprocess.TimeoutExpired:
|
|
79
|
-
return False, "timeout"
|
|
80
|
-
except Exception as e:
|
|
81
|
-
return False, str(e)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _compress_ast(file_path: str, svc, finalize, maybe_facts) -> str:
|
|
85
|
-
"""Use codebase-memory-mcp binary for AST knowledge-graph structural analysis."""
|
|
86
|
-
cwd = str(svc.project_path)
|
|
87
|
-
ok, out = _run_memory_mcp_cli(["cli", "get_architecture"], cwd)
|
|
88
|
-
|
|
89
|
-
if not ok and out == "not_installed":
|
|
90
|
-
return finalize(
|
|
91
|
-
"c3_compress", {"file_path": file_path, "mode": "ast"},
|
|
92
|
-
"[compress:ast] codebase-memory-mcp not installed.\n"
|
|
93
|
-
"Install (macOS/Linux): curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash\n"
|
|
94
|
-
"Install (Windows): see https://github.com/DeusData/codebase-memory-mcp\n"
|
|
95
|
-
"Tip: use c3_compress(mode='map') for C3's built-in structural analysis.",
|
|
96
|
-
"not_installed",
|
|
97
|
-
)
|
|
98
|
-
if not ok:
|
|
99
|
-
hint = "Run: codebase-memory-mcp index" if not out or "not indexed" in out.lower() else out
|
|
100
|
-
return finalize(
|
|
101
|
-
"c3_compress", {"file_path": file_path, "mode": "ast"},
|
|
102
|
-
f"[compress:ast] {hint}\nTip: say 'Index this project' with codebase-memory-mcp active.",
|
|
103
|
-
"not_indexed",
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
try:
|
|
107
|
-
data = json.loads(out)
|
|
108
|
-
except json.JSONDecodeError:
|
|
109
|
-
return finalize("c3_compress", {"file_path": file_path, "mode": "ast"},
|
|
110
|
-
f"[compress:ast] Unexpected output — try: codebase-memory-mcp index\n{out[:200]}",
|
|
111
|
-
"error")
|
|
112
|
-
|
|
113
|
-
lines = ["[compress:ast] codebase-memory-mcp knowledge graph"]
|
|
114
|
-
if data.get("languages"):
|
|
115
|
-
lines.append(f"Languages: {', '.join(str(l) for l in data['languages'])}")
|
|
116
|
-
if data.get("entry_points"):
|
|
117
|
-
eps = [str(e) for e in data["entry_points"][:6]]
|
|
118
|
-
lines.append(f"Entry points: {', '.join(eps)}")
|
|
119
|
-
if data.get("packages"):
|
|
120
|
-
pkgs = [str(p) for p in data["packages"][:12]]
|
|
121
|
-
lines.append(f"Packages: {', '.join(pkgs)}")
|
|
122
|
-
if data.get("layers"):
|
|
123
|
-
layers = [str(l) for l in data["layers"][:8]]
|
|
124
|
-
lines.append(f"Layers: {', '.join(layers)}")
|
|
125
|
-
if data.get("hotspots"):
|
|
126
|
-
lines.append("Hotspots (high-churn):")
|
|
127
|
-
for h in data["hotspots"][:6]:
|
|
128
|
-
lines.append(f" {h}")
|
|
129
|
-
if data.get("clusters"):
|
|
130
|
-
lines.append(f"Clusters: {len(data['clusters'])} functional modules detected")
|
|
131
|
-
if data.get("routes"):
|
|
132
|
-
lines.append(f"Routes: {len(data['routes'])} HTTP endpoints")
|
|
133
|
-
|
|
134
|
-
return finalize("c3_compress", {"file_path": file_path, "mode": "ast"},
|
|
135
|
-
"\n".join(lines), "ast")
|
|
136
|
-
|
|
137
|
-
|
|
138
71
|
def handle_compress(file_path: str, mode: str, svc,
|
|
139
72
|
finalize, maybe_facts) -> str:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if mode in ("structure", "outline"):
|
|
147
|
-
mode = "map"
|
|
148
|
-
else:
|
|
149
|
-
return finalize("c3_compress", {"file_path": file_path, "mode": mode},
|
|
150
|
-
f"[compress:error] Unknown mode '{mode}'. Use: {', '.join(valid_modes)}",
|
|
151
|
-
"error")
|
|
73
|
+
requested = (mode or "map").strip() or "map"
|
|
74
|
+
if requested != "map" and requested not in RETIRED_MODES:
|
|
75
|
+
return finalize("c3_compress", {"file_path": file_path, "mode": requested},
|
|
76
|
+
f"[compress:error] Unknown mode '{requested}'. The only mode is 'map'.",
|
|
77
|
+
"error")
|
|
78
|
+
note = deprecation_line(requested) if requested != "map" else ""
|
|
152
79
|
|
|
153
80
|
# Batch dispatch: comma-separated paths
|
|
154
81
|
if "," in file_path:
|
|
155
82
|
paths = [p.strip() for p in file_path.split(",") if p.strip()]
|
|
156
|
-
return _compress_batch(paths,
|
|
83
|
+
return _compress_batch(paths, requested, note, svc, finalize)
|
|
84
|
+
|
|
85
|
+
return _compress_single(file_path, requested, note, svc, finalize)
|
|
157
86
|
|
|
158
|
-
|
|
87
|
+
|
|
88
|
+
def _build_map(svc, rel: str) -> str:
|
|
89
|
+
"""The canonical map for `rel`, draining any queued watcher updates first."""
|
|
90
|
+
queued = svc.file_memory.drain_queue()
|
|
91
|
+
completed = []
|
|
92
|
+
failed = []
|
|
93
|
+
for qp in queued[:10]:
|
|
94
|
+
try:
|
|
95
|
+
if svc.file_memory.update(qp):
|
|
96
|
+
completed.append(qp)
|
|
97
|
+
else:
|
|
98
|
+
failed.append(qp)
|
|
99
|
+
except Exception:
|
|
100
|
+
failed.append(qp)
|
|
101
|
+
if completed:
|
|
102
|
+
svc.file_memory.complete_updates(completed)
|
|
103
|
+
if failed:
|
|
104
|
+
svc.file_memory.complete_updates(failed, failed=True)
|
|
105
|
+
return svc.file_memory.get_or_build_map(rel)
|
|
159
106
|
|
|
160
107
|
|
|
161
|
-
def _compress_single(file_path: str,
|
|
162
|
-
"""
|
|
163
|
-
# Access Guard: read verdict up front
|
|
164
|
-
# that never reach compressor.compress_file (docs/access-guard.md §3).
|
|
108
|
+
def _compress_single(file_path: str, requested: str, note: str, svc, finalize) -> str:
|
|
109
|
+
"""Map a single file."""
|
|
110
|
+
# Access Guard: read verdict up front (docs/access-guard.md §3).
|
|
165
111
|
_verdict = access_guard.verdict(file_path, "read", svc.project_path)
|
|
166
112
|
if _verdict.denial and not _grants.allow(svc, _verdict.denial,
|
|
167
113
|
tool="c3_compress", op="read",
|
|
168
114
|
path=file_path):
|
|
169
|
-
return finalize("c3_compress", {"file_path": file_path, "mode":
|
|
115
|
+
return finalize("c3_compress", {"file_path": file_path, "mode": requested},
|
|
170
116
|
access_guard.refusal(_verdict.denial, file_path, "read"),
|
|
171
117
|
"access-denied")
|
|
172
|
-
# Mask Guard: the map
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
if _verdict.masked and mode in ("map", "dense_map"):
|
|
118
|
+
# Mask Guard: the map is built through file_memory, which reads the RAW
|
|
119
|
+
# file — a masked path is served from its view instead
|
|
120
|
+
# (docs/mask-guard.md §6, row 7).
|
|
121
|
+
if _verdict.masked:
|
|
177
122
|
from services import mask_mirror
|
|
178
123
|
try:
|
|
179
124
|
view = mask_mirror.render_for_path(file_path, svc.project_path)
|
|
180
125
|
except mask_mirror.MaskUnavailable as exc:
|
|
181
126
|
return finalize("c3_compress",
|
|
182
|
-
{"file_path": file_path, "mode":
|
|
127
|
+
{"file_path": file_path, "mode": requested},
|
|
183
128
|
f"{access_guard.TAG_MASK_UNSUPPORTED} {exc.message}",
|
|
184
129
|
"mask-unavailable")
|
|
185
|
-
return finalize("c3_compress", {"file_path": file_path, "mode":
|
|
130
|
+
return finalize("c3_compress", {"file_path": file_path, "mode": requested},
|
|
186
131
|
view.with_header(file_path), f"masked:{view.preset}")
|
|
187
132
|
|
|
188
133
|
full = Path(svc.project_path) / file_path
|
|
189
134
|
if not full.exists():
|
|
190
135
|
full = Path(file_path)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
completed = []
|
|
201
|
-
failed = []
|
|
202
|
-
for qp in queued[:10]:
|
|
203
|
-
try:
|
|
204
|
-
if svc.file_memory.update(qp):
|
|
205
|
-
completed.append(qp)
|
|
206
|
-
else:
|
|
207
|
-
failed.append(qp)
|
|
208
|
-
except Exception:
|
|
209
|
-
failed.append(qp)
|
|
210
|
-
if completed:
|
|
211
|
-
svc.file_memory.complete_updates(completed)
|
|
212
|
-
if failed:
|
|
213
|
-
svc.file_memory.complete_updates(failed, failed=True)
|
|
214
|
-
res = (svc.file_memory.get_or_build_dense_map(rel)
|
|
215
|
-
if mode == "dense_map"
|
|
216
|
-
else svc.file_memory.get_or_build_map(rel))
|
|
217
|
-
# Structured accounting: pass the (full-read baseline, map) pair via
|
|
218
|
-
# record_tool_tokens() instead of a "raw->maptok" summary for the
|
|
219
|
-
# legacy regex fallback to scrape.
|
|
220
|
-
raw_tokens = None
|
|
221
|
-
map_tokens = 0
|
|
222
|
-
try:
|
|
223
|
-
raw_text = full.read_text(encoding="utf-8", errors="replace")
|
|
224
|
-
raw_tokens = count_tokens(raw_text)
|
|
225
|
-
map_tokens = count_tokens(res)
|
|
226
|
-
if map_tokens >= raw_tokens and not res.startswith("[file_map]"):
|
|
227
|
-
# A map that costs more than the file is worth nothing
|
|
228
|
-
# (docs/file-map.md § Small files).
|
|
229
|
-
res = f"[compress:{file_path}] whole file — smaller than its map\n" + raw_text
|
|
230
|
-
map_tokens = count_tokens(res)
|
|
231
|
-
summary = mode
|
|
232
|
-
except Exception:
|
|
233
|
-
summary = "mapped"
|
|
234
|
-
return finalize_with_tokens(
|
|
235
|
-
finalize, svc, "c3_compress", {"file_path": file_path, "mode": mode},
|
|
236
|
-
res, summary,
|
|
237
|
-
raw_tokens=raw_tokens, optimized_tokens=map_tokens or None,
|
|
238
|
-
response_tokens=map_tokens,
|
|
239
|
-
detail=map_detail(svc, rel, mode, cache_hit=cache_hit))
|
|
240
|
-
|
|
136
|
+
if not full.exists():
|
|
137
|
+
return ("[file_map:error] not found. To create a new file use "
|
|
138
|
+
"c3_edit(file_path=..., old_string='', new_string=<content>).")
|
|
139
|
+
rel = str(full.resolve().relative_to(Path(svc.project_path).resolve())).replace("\\", "/")
|
|
140
|
+
# C0 measurement: was the record already fresh before this call?
|
|
141
|
+
cache_hit = not svc.file_memory.needs_update(rel)
|
|
142
|
+
res = _build_map(svc, rel)
|
|
143
|
+
raw_tokens = None
|
|
144
|
+
map_tokens = 0
|
|
241
145
|
try:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
146
|
+
raw_text = full.read_text(encoding="utf-8", errors="replace")
|
|
147
|
+
raw_tokens = count_tokens(raw_text)
|
|
148
|
+
map_tokens = count_tokens(res)
|
|
149
|
+
if map_tokens >= raw_tokens and not res.startswith("[file_map]"):
|
|
150
|
+
# A map that costs more than the file is worth nothing
|
|
151
|
+
# (docs/file-map.md § Small files).
|
|
152
|
+
res = f"[compress:{file_path}] whole file — smaller than its map\n" + raw_text
|
|
153
|
+
map_tokens = count_tokens(res)
|
|
154
|
+
summary = "map"
|
|
155
|
+
except Exception:
|
|
156
|
+
summary = "mapped"
|
|
157
|
+
if note:
|
|
158
|
+
res = note + "\n" + res
|
|
159
|
+
map_tokens = count_tokens(res)
|
|
249
160
|
return finalize_with_tokens(
|
|
250
|
-
finalize, svc, "c3_compress", {"file_path": file_path},
|
|
251
|
-
|
|
252
|
-
raw_tokens=
|
|
253
|
-
|
|
254
|
-
detail=
|
|
161
|
+
finalize, svc, "c3_compress", {"file_path": file_path, "mode": requested},
|
|
162
|
+
res, summary,
|
|
163
|
+
raw_tokens=raw_tokens, optimized_tokens=map_tokens or None,
|
|
164
|
+
response_tokens=map_tokens,
|
|
165
|
+
detail=map_detail(svc, rel, requested, cache_hit=cache_hit))
|
|
255
166
|
|
|
256
167
|
|
|
257
|
-
def _compress_batch(paths: list,
|
|
258
|
-
"""
|
|
168
|
+
def _compress_batch(paths: list, requested: str, note: str, svc, finalize) -> str:
|
|
169
|
+
"""Map several files in parallel, return one combined report."""
|
|
259
170
|
max_files = 10
|
|
260
171
|
paths = paths[:max_files]
|
|
261
172
|
|
|
262
173
|
results = {}
|
|
263
174
|
|
|
264
175
|
def _do_one(fp):
|
|
265
|
-
"""Returns (fp,
|
|
176
|
+
"""Returns (fp, map_text, raw_tokens, map_tokens, error)."""
|
|
266
177
|
try:
|
|
267
178
|
# Access Guard: per-member read verdict — the S1 refusal becomes
|
|
268
179
|
# this member's batch line; other members are still served.
|
|
@@ -271,8 +182,6 @@ def _compress_batch(paths: list, mode: str, svc, finalize, maybe_facts) -> str:
|
|
|
271
182
|
return fp, None, None, None, access_guard.refusal(_v.denial, fp,
|
|
272
183
|
"read")
|
|
273
184
|
if _v.masked:
|
|
274
|
-
# Serve this member from its view; map modes would otherwise
|
|
275
|
-
# build from raw bytes via file_memory.
|
|
276
185
|
from services import mask_mirror
|
|
277
186
|
try:
|
|
278
187
|
view = mask_mirror.render_for_path(fp, svc.project_path)
|
|
@@ -286,33 +195,23 @@ def _compress_batch(paths: list, mode: str, svc, finalize, maybe_facts) -> str:
|
|
|
286
195
|
full = Path(fp)
|
|
287
196
|
if not full.exists():
|
|
288
197
|
return fp, None, None, None, "not found"
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
map_tok = count_tokens(res)
|
|
299
|
-
return fp, res, raw_tok, map_tok, None
|
|
300
|
-
except Exception:
|
|
301
|
-
return fp, res, None, None, None
|
|
302
|
-
else:
|
|
303
|
-
res = svc.compressor.compress_file(str(full), mode)
|
|
304
|
-
if "error" in res:
|
|
305
|
-
return fp, None, None, None, res["error"]
|
|
306
|
-
return (fp, res["compressed"], res.get("original_tokens"),
|
|
307
|
-
res.get("compressed_tokens"), None)
|
|
198
|
+
rel = str(full.resolve().relative_to(
|
|
199
|
+
Path(svc.project_path).resolve())).replace("\\", "/")
|
|
200
|
+
res = svc.file_memory.get_or_build_map(rel)
|
|
201
|
+
try:
|
|
202
|
+
raw_tok = count_tokens(full.read_text(encoding="utf-8", errors="replace"))
|
|
203
|
+
map_tok = count_tokens(res)
|
|
204
|
+
return fp, res, raw_tok, map_tok, None
|
|
205
|
+
except Exception:
|
|
206
|
+
return fp, res, None, None, None
|
|
308
207
|
except Exception as e:
|
|
309
208
|
return fp, None, None, None, str(e)
|
|
310
209
|
|
|
311
210
|
with ThreadPoolExecutor(max_workers=min(len(paths), 8)) as pool:
|
|
312
211
|
futures = {pool.submit(_do_one, fp): fp for fp in paths}
|
|
313
212
|
for fut in as_completed(futures):
|
|
314
|
-
fp,
|
|
315
|
-
results[fp] = (
|
|
213
|
+
fp, text, raw_tok, opt_tok, err = fut.result()
|
|
214
|
+
results[fp] = (text, raw_tok, opt_tok, err)
|
|
316
215
|
|
|
317
216
|
ratios = show_token_ratios(svc)
|
|
318
217
|
parts = []
|
|
@@ -321,8 +220,8 @@ def _compress_batch(paths: list, mode: str, svc, finalize, maybe_facts) -> str:
|
|
|
321
220
|
total_opt = 0
|
|
322
221
|
measured = 0
|
|
323
222
|
for fp in paths:
|
|
324
|
-
|
|
325
|
-
if
|
|
223
|
+
text, raw_tok, opt_tok, err = results.get(fp, (None, None, None, "unknown"))
|
|
224
|
+
if text:
|
|
326
225
|
tag = ""
|
|
327
226
|
if raw_tok is not None and opt_tok is not None:
|
|
328
227
|
measured += 1
|
|
@@ -330,18 +229,23 @@ def _compress_batch(paths: list, mode: str, svc, finalize, maybe_facts) -> str:
|
|
|
330
229
|
total_opt += opt_tok
|
|
331
230
|
if ratios:
|
|
332
231
|
tag = f" ({raw_tok}->{opt_tok}tok)"
|
|
333
|
-
parts.append(f"## {fp}{tag}\n{
|
|
232
|
+
parts.append(f"## {fp}{tag}\n{text}")
|
|
334
233
|
total_ok += 1
|
|
335
234
|
else:
|
|
336
235
|
parts.append(f"## {fp} — ERROR: {err}")
|
|
337
236
|
|
|
338
|
-
header = f"[compress:batch] {total_ok}/{len(paths)} files (
|
|
237
|
+
header = f"[compress:batch] {total_ok}/{len(paths)} files (map)"
|
|
238
|
+
if note:
|
|
239
|
+
header = note + "\n" + header
|
|
339
240
|
body = header + "\n\n" + "\n\n".join(parts)
|
|
241
|
+
detail = {"requested_mode": requested, "backend": "batch",
|
|
242
|
+
"files": len(paths), "ok": total_ok}
|
|
243
|
+
if requested != "map":
|
|
244
|
+
detail["deprecated_mode"] = requested
|
|
340
245
|
return finalize_with_tokens(
|
|
341
246
|
finalize, svc, "c3_compress",
|
|
342
|
-
{"file_path": ",".join(paths), "mode":
|
|
247
|
+
{"file_path": ",".join(paths), "mode": requested, "batch": True},
|
|
343
248
|
body, f"batch {total_ok}/{len(paths)}",
|
|
344
249
|
raw_tokens=total_raw if measured else None,
|
|
345
250
|
optimized_tokens=total_opt if measured else None,
|
|
346
|
-
detail=
|
|
347
|
-
"files": len(paths), "ok": total_ok})
|
|
251
|
+
detail=detail)
|
cli/tools/read.py
CHANGED
|
@@ -177,6 +177,22 @@ def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
|
|
|
177
177
|
|
|
178
178
|
rel_path = str(full.resolve().relative_to(Path(svc.project_path).resolve())).replace("\\", "/")
|
|
179
179
|
|
|
180
|
+
if full.is_dir():
|
|
181
|
+
# A directory maps to one line per file, budgeted
|
|
182
|
+
# (docs/file-map.md § Directories). `lines=<int>` sets the budget.
|
|
183
|
+
from services.dir_map import DEFAULT_MAX_TOKENS, render_directory_map
|
|
184
|
+
budget = DEFAULT_MAX_TOKENS
|
|
185
|
+
if isinstance(lines, int) and not isinstance(lines, bool) and lines > 0:
|
|
186
|
+
budget = lines
|
|
187
|
+
text, dir_detail = render_directory_map(svc, rel_path, max_tokens=budget)
|
|
188
|
+
tok = count_tokens(text)
|
|
189
|
+
if finalize is None:
|
|
190
|
+
return text
|
|
191
|
+
return finalize_with_tokens(
|
|
192
|
+
finalize, svc, "c3_read", {"file": file_path}, text,
|
|
193
|
+
f"dir:{dir_detail['files']}files->{tok}tok",
|
|
194
|
+
optimized_tokens=tok, response_tokens=tok, detail=dir_detail)
|
|
195
|
+
|
|
180
196
|
# Resolve ranges
|
|
181
197
|
ranges = []
|
|
182
198
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.123.0
|
|
4
4
|
Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=e48F6FQloFReUIMtmWsih2t54j9SROzAaHtMkEuttDs,20489
|
|
3
3
|
cli/_shell_writes.py,sha256=XzUNlUDljmg-wZxK03xHnRe36WeQxe4tpO9HKVFwP5o,5090
|
|
4
|
-
cli/c3.py,sha256=
|
|
4
|
+
cli/c3.py,sha256=bec4nDHziMA74g4aEvg-_X0nQKCLmNNkTr2gslczpHg,416843
|
|
5
5
|
cli/docs.html,sha256=X9M7qC7LWi8pPJNF-4yEM57jhuZ3WeTESECbdDOeNaA,143172
|
|
6
6
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
7
7
|
cli/hook_access_guard.py,sha256=ba8-un6G9Ap8HCzx7nyhVq3c_G4sOtiVXRy0BzzX8-Y,23349
|
|
@@ -15,7 +15,7 @@ cli/hook_edit_ledger.py,sha256=3ooM6Lv83Ntq_aeyrZdn-0FlZSsE_CrWeuB_V-SEAJk,12105
|
|
|
15
15
|
cli/hook_edit_unlock.py,sha256=PwPRuCQPARZZRtH6wb2Xm-6ADpfrC-0rXs2uRmHAAmg,6883
|
|
16
16
|
cli/hook_filter.py,sha256=v8ZSznK5StTfYYqae1l4V2bm-e9sy2RFfpbNr5KwE2U,4988
|
|
17
17
|
cli/hook_ghost_files.py,sha256=plbedFgIJ5tXEE_8USUE39RmyODh1UjIUFNRbUo4R8w,10291
|
|
18
|
-
cli/hook_pretool_enforce.py,sha256=
|
|
18
|
+
cli/hook_pretool_enforce.py,sha256=k9jEyi7ip7MnsbBK3jE0JEcLEitHMg2iMWnX56XhaBY,31329
|
|
19
19
|
cli/hook_prompt_recall.py,sha256=LHyubu36pf5h3hbLBe0yH-yGhAlwLJdsc7hfa7WB1h4,3471
|
|
20
20
|
cli/hook_read.py,sha256=ZWZldgqGSNz78COhlgETQ7yxdOW5vWIIHUet9gZKhxk,8176
|
|
21
21
|
cli/hook_session_stats.py,sha256=i1k7DM0RoQsHm5R8C8RUWvpEA7lzP-NPD95-fAARlnI,7273
|
|
@@ -30,18 +30,18 @@ cli/ui.html,sha256=xcdt74nlFEXx-0Bx6-Okw-WSVZPAXL0iukxU0ytI6CA,5694
|
|
|
30
30
|
cli/ui_legacy.html,sha256=cI8tC6RKmE2NIJOcsu7CY-zT4VznjcbD6NTjxb_fvUY,378460
|
|
31
31
|
cli/ui_nano.html,sha256=UAwQ6bbTOXAoGq191AZ7slhngR9edJSa3IhqpynveDg,27740
|
|
32
32
|
cli/commands/__init__.py,sha256=0Z8MABNzwSFJGT4Xv9R5AJVR8XxraTsuVTz5b0bShmo,38
|
|
33
|
-
cli/commands/common.py,sha256=
|
|
34
|
-
cli/commands/parser.py,sha256=
|
|
33
|
+
cli/commands/common.py,sha256=X5sjog7O274vmmVOMe3pO2QzbBOdhp-nWKSj7wqi8SA,14411
|
|
34
|
+
cli/commands/parser.py,sha256=OQMrKiZzk3LVaCsn3dNEm34fXHnu5zxZvttMEoeJGcI,65466
|
|
35
35
|
cli/guide/access.html,sha256=BelnEc4p7XgcH_ysTlMXL9ZHCD0O4CPLq3h5rvqPIh8,14790
|
|
36
36
|
cli/guide/bitbucket.html,sha256=6zmHIv2-G314_HDqMCQV-ZAIy0-co0gaFs0_k2Q1kYc,35491
|
|
37
37
|
cli/guide/credentials.html,sha256=801vO_cCqG7oHsa73yAGSoBDGZ4mcGsdx6D2uS1dwCQ,58196
|
|
38
38
|
cli/guide/getting-started.html,sha256=VAKnff0tDFGPobVAihHW1-hjUfAkxQrpVJg6yKcWcbg,21653
|
|
39
|
-
cli/guide/index.html,sha256=
|
|
39
|
+
cli/guide/index.html,sha256=nNM_gAi3e8SGEUV7jIsd8N3kmAP35BepwmjZ2HuJoSA,22869
|
|
40
40
|
cli/guide/masking.html,sha256=Jz8fEwp5XwudUafn0Ymytofpf4PPWRpyxOHMl09dD5o,19203
|
|
41
41
|
cli/guide/oracle.html,sha256=66utgqoOpHXxZBEopnw-Q-tvhoQOD82YckXW2xDTTJ4,25152
|
|
42
42
|
cli/guide/shared.css,sha256=Mmm7W6aYxrkDq2RPOcyXiQFYGR-yB3auwX1uIWi6C74,15967
|
|
43
43
|
cli/guide/tools.html,sha256=KrTwffccJrkxiEhn1TBdyN2qUzOuL_uIUxR0wH39pk0,92693
|
|
44
|
-
cli/guide/workflow.html,sha256
|
|
44
|
+
cli/guide/workflow.html,sha256=-VyLgULPDsucuN1N8D6Vc6g-VMzUIfLVpVah_pDekfk,64582
|
|
45
45
|
cli/hub_ui/app.js,sha256=Gbk-2DxqUynrVqJbS8pNUEHmcCpjuUhqeDxWUWa6e_A,6984
|
|
46
46
|
cli/hub_ui/state.js,sha256=yQXsN0G1YCvtlv6aoIs0iqY7DNfhQ0ybj7Ig9wbBuY4,7279
|
|
47
47
|
cli/hub_ui/components/add_project.js,sha256=j5m0mX0gePcEYQW_0vodJkw5v8Vn2lQqPAI0miIGdv4,3182
|
|
@@ -78,7 +78,7 @@ cli/tools/agent.py,sha256=RmEEh5HhCZLvYu0gSRcwA_NENpd4iIYJW3exBzT8kVs,47983
|
|
|
78
78
|
cli/tools/artifacts.py,sha256=SjazShDFIzOpAZW7uMv9itBktXnc6pqQKZeaen8GgzI,9379
|
|
79
79
|
cli/tools/bitbucket.py,sha256=cni8Y1EoiNm9jcHEpzi2dgKFIT1-4ct0WOhpTs7ac3k,35812
|
|
80
80
|
cli/tools/ci.py,sha256=fQ03RENywS5gqHjH26Fa4YiraBTvchlPcPSnVYwwKBs,19444
|
|
81
|
-
cli/tools/compress.py,sha256=
|
|
81
|
+
cli/tools/compress.py,sha256=f8DNJlr49cVibvuOSyChHjytzbB30m7uXW3zWq8mVAU,10819
|
|
82
82
|
cli/tools/credentials.py,sha256=R8Errpa41vsq0Z_sb7K-fmJxVovzXDYUyniITLrFC24,19033
|
|
83
83
|
cli/tools/delegate.py,sha256=yA2-ph53Y8XAJMpz7R5LwqXshRRD8bYljgN244RbHWA,62012
|
|
84
84
|
cli/tools/edit.py,sha256=xnRRM2UUybvGvfSzXzIjWmt6_zNaCbkupiK1z-8K4jE,30127
|
|
@@ -92,7 +92,7 @@ cli/tools/locks.py,sha256=FvuNxToU-5ZE2m_C6fTrn1jKERoxR4cbtxNb_b8mXqA,5882
|
|
|
92
92
|
cli/tools/memory.py,sha256=yDDRsEngeFcjb6nXUrhRZXuboasXfixJeyltKhDbZD4,24935
|
|
93
93
|
cli/tools/override.py,sha256=LiZFgoDN-35NXVwldnYrcCzfBl8MhQQ3tKVVkOG5d_o,9217
|
|
94
94
|
cli/tools/project.py,sha256=MWzi7tBTQ03p_6PavmG9ZLXZHO0IhjKEJNyB0oZWTFc,24248
|
|
95
|
-
cli/tools/read.py,sha256=
|
|
95
|
+
cli/tools/read.py,sha256=xZrKGABR-smsfGfg5CUfv77opA9EsOWnNhiYVj9hymQ,18421
|
|
96
96
|
cli/tools/search.py,sha256=BZh3E7YggjCWOC1lJz0YNcRsdDLWsJiEmONtgUW2rEo,32397
|
|
97
97
|
cli/tools/session.py,sha256=ailQiRqXhi9NcBB3XS3Dv72GEkEZaYivB0bqDlhmgTA,5202
|
|
98
98
|
cli/tools/shell.py,sha256=Zcsocl8IXeL9DSQNewq_fXECSufTccUdwc-H3S2Alto,58818
|
|
@@ -123,7 +123,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
123
123
|
cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
|
|
124
124
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
125
125
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
126
|
-
code_context_control-2.
|
|
126
|
+
code_context_control-2.123.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
127
127
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
128
128
|
core/codex_patch.py,sha256=CxvT_rQIwPNTRJZj9EHJmx0ZdOeSLxjiixuLI3Z7lPc,2107
|
|
129
129
|
core/config.py,sha256=YmkcZwedz_lfDM0ZuI_f4xpe98ixPLcSQFcTCHgRiRg,19206
|
|
@@ -200,15 +200,16 @@ services/ci_intel.py,sha256=frr1jL-FXU_JNUoRW8TwnZo04fqUElAGGEsDEvgIeUs,7161
|
|
|
200
200
|
services/ci_runner.py,sha256=egkFGaOjAa45KIMFbGX7108R2wUQwUuLgJ4Iq7ygx9I,40768
|
|
201
201
|
services/ci_workflow.py,sha256=xfeUvzcm9GHiDdi7QAbg4SAvnzzuCCV3bmwdAMUTZKA,31133
|
|
202
202
|
services/circuit_breaker.py,sha256=ES6PpjL40gfDhtK88GeS6GFzsz5EOEEkZsB-DzX_tbw,3179
|
|
203
|
-
services/claude_md.py,sha256=
|
|
204
|
-
services/codex_integration.py,sha256=
|
|
203
|
+
services/claude_md.py,sha256=U9Ogzktmqz1D75L_vEBuadNqddrhUY1v_e3KbPn46EE,61453
|
|
204
|
+
services/codex_integration.py,sha256=px7Mcyl8d3TlJxMyjzhuysuz0caaqWh023UvW2CdVjE,7716
|
|
205
205
|
services/codex_transcript.py,sha256=TtxKiNkZ3vsWpaQRczaIGsA7wd8r24z_65Nnso2BNj0,3658
|
|
206
|
-
services/compressor.py,sha256=
|
|
206
|
+
services/compressor.py,sha256=TnszNgnikaWR2CgUe4wO8NQ2ayNzhMExY42IVPMV5w4,33222
|
|
207
207
|
services/context_snapshot.py,sha256=2f_bxY3xX4ZC653ncYno8YSeSTFSavwBjrorytHA0Ns,16879
|
|
208
208
|
services/conversation_store.py,sha256=RKA0nk09VAI8Ip935kSVlQdF-cQEzFLrgHbZiEJ7G1E,37518
|
|
209
209
|
services/cred_audit.py,sha256=tdxnRzbB0c8nJ7dqA9HOJrcOeRWivEzq6yhwu6ksmZI,10240
|
|
210
210
|
services/cred_telemetry.py,sha256=LwDd4x0Mu6YXeDUrgICHnX9o1Ym_4hoouRp98qx0zlI,10926
|
|
211
211
|
services/credential_store.py,sha256=x8ejkSJAqjlUHwCdJ-AyOrLJwSpMCUmuq61nkBTrm-Y,68797
|
|
212
|
+
services/dir_map.py,sha256=r40hj8PN6lDnKGFjDsslsMQJhbdMkleIksHtYTw4sQA,6666
|
|
212
213
|
services/doc_index.py,sha256=cIb1jk4GrKK7cLwD6phoc04TvKA7Ee94kKHXxB7tT_k,19223
|
|
213
214
|
services/e2e_benchmark.py,sha256=5Z3rE42o08MKVbIX_769V0RX2yAzSVcApCvdAQ9YLAU,134988
|
|
214
215
|
services/e2e_evaluator.py,sha256=FsB4vMzcFUcmicfIwRtA8JZ_XeMpuk2_-86F2iVNEDc,17783
|
|
@@ -305,8 +306,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
305
306
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
306
307
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
307
308
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
308
|
-
code_context_control-2.
|
|
309
|
-
code_context_control-2.
|
|
310
|
-
code_context_control-2.
|
|
311
|
-
code_context_control-2.
|
|
312
|
-
code_context_control-2.
|
|
309
|
+
code_context_control-2.123.0.dist-info/METADATA,sha256=GA2nhOCq4RoLbjrfyct4SK7y0hRqiShnQhpUZnD78DQ,27067
|
|
310
|
+
code_context_control-2.123.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
311
|
+
code_context_control-2.123.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
312
|
+
code_context_control-2.123.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
313
|
+
code_context_control-2.123.0.dist-info/RECORD,,
|
services/claude_md.py
CHANGED
|
@@ -32,7 +32,7 @@ When falling back, state which c3_* tool was attempted and why it was insufficie
|
|
|
32
32
|
## Workflow (follow this order — do not skip steps)
|
|
33
33
|
1. **RECALL**: `c3_memory(action='recall')` — before any multi-step or context-dependent task. Large memory stores: use `index` first (compact list), then `fetch` for specific IDs
|
|
34
34
|
2. **SEARCH FIRST**: `c3_search(action='code|files|semantic')` — before ANY file discovery or content search. Never start with Grep/Glob
|
|
35
|
-
3. **MAP before READ**: `
|
|
35
|
+
3. **MAP before READ**: `c3_read(file_path)` with no symbols/lines returns the file map (one line per symbol with signature and `[La-Lb]` range, ~10% of the file's tokens; a directory path maps its files), then `c3_read(symbols=['Class.method']|lines=[a,b])` for the source you need. Never start with native Read. `c3_compress` is the same map for a comma-separated batch (legacy; removed in 2.124.0)
|
|
36
36
|
4. **IMPACT** (shared symbols): `c3_impact(target='symbol')` — blast-radius check before editing any function/class used across files
|
|
37
37
|
5. **EDIT via C3**: `c3_edit(file_path, old_string, new_string, summary)` — for ALL edits. Parallel across files; `edits=[]` batch for same file
|
|
38
38
|
6. **FILTER**: `c3_filter(text=...)` — for terminal output >10 lines or log files
|
|
@@ -55,7 +55,7 @@ In plan mode, all c3_* read tools (search, read, compress, filter, validate, sta
|
|
|
55
55
|
## Anti-patterns (DO NOT do these)
|
|
56
56
|
- Starting with native file search/read/grep without a prior c3_* call
|
|
57
57
|
- Using native Edit when c3_edit is available
|
|
58
|
-
- Reading entire files when
|
|
58
|
+
- Reading entire files when a c3_read map + a symbol read would be more surgical
|
|
59
59
|
- Skipping c3_validate after making edits"""
|
|
60
60
|
|
|
61
61
|
# Pointer that replaces the embedded Project Context tree (v2.60.0).
|
|
@@ -73,14 +73,14 @@ C3_NANO_WORKFLOW = """\
|
|
|
73
73
|
## C3 Tools — MANDATORY
|
|
74
74
|
Native tools BLOCKED unless c3_* called first. State reason when falling back.
|
|
75
75
|
1. c3_search(action='code|files|semantic') — BEFORE any search/grep/glob
|
|
76
|
-
2.
|
|
76
|
+
2. c3_read(file_path) maps a file (or directory); then c3_read(symbols=['Class.method']|lines=[a,b]) — BEFORE any file read
|
|
77
77
|
3. c3_edit(file_path, old_string, new_string, summary) — for ALL edits; edits=[{...}] batch
|
|
78
78
|
4. c3_filter(text='...') — output >10 lines
|
|
79
79
|
5. c3_validate(file_path) — after edits
|
|
80
80
|
6. c3_session(action='log'|'snapshot') — decisions / before /clear
|
|
81
81
|
7. `[c3-access:confirm]` = PAUSE, not block: a human approves this exact write (agent-config files — .mcp.json, instruction docs, .claude bodies — hold by default). Read the S8 tail: wait on the request it names (c3_override(action='wait', request_id='...', timeout_s=180)), then retry once. Never retry early, never route around it.
|
|
82
82
|
Plan mode: all c3_* read tools work normally — skip edit/delegate steps.
|
|
83
|
-
DO NOT: start with native Read/Grep/Glob/Edit, skip c3_validate, read full files without
|
|
83
|
+
DO NOT: start with native Read/Grep/Glob/Edit, skip c3_validate, read full files without mapping them first."""
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
# --- Per-IDE workflow adaptation ----------------------------------------------
|
services/codex_integration.py
CHANGED
|
@@ -8,7 +8,7 @@ CODEX_WORKFLOW = """# C3 — Codex workflow
|
|
|
8
8
|
|
|
9
9
|
Use C3 for repository intelligence and audited changes:
|
|
10
10
|
1. Recall relevant project facts with c3_memory; inspect .c3/MAP.md.
|
|
11
|
-
2. Find candidates with c3_search; map with
|
|
11
|
+
2. Find candidates with c3_search; map with c3_read(file_path) (no symbols), then
|
|
12
12
|
read exact symbols or lines with c3_read. Check shared changes with c3_impact.
|
|
13
13
|
3. Edit with c3_edit, validate with c3_validate, and record decisions with
|
|
14
14
|
c3_session(action='log'). Preserve unrelated work.
|
services/compressor.py
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
AST-based Code Compression Service
|
|
3
3
|
|
|
4
4
|
Parses source files into AST and generates compressed summaries:
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
-
|
|
5
|
+
- "map": the canonical file map (services/file_map) — what the tools serve
|
|
6
|
+
- "structure" / "outline" / "smart": signature-only summaries kept for
|
|
7
|
+
internal callers (delegate, Hub REST, context snapshots, benchmarks)
|
|
8
|
+
- "summary": an LLM summary through the router, when one is configured
|
|
9
|
+
|
|
10
|
+
"diff" and "bug_scan" were retired in 2.122.0 (docs/file-map.md).
|
|
9
11
|
"""
|
|
10
12
|
import hashlib
|
|
11
13
|
import json
|
|
@@ -145,6 +147,30 @@ class CodeCompressor:
|
|
|
145
147
|
if protected_files:
|
|
146
148
|
self._protected_files.update(self._normalize_rel_path(p) for p in protected_files)
|
|
147
149
|
self.router = router
|
|
150
|
+
self._sweep_retired_caches()
|
|
151
|
+
|
|
152
|
+
#: Modes the compressor no longer implements, and where to go instead.
|
|
153
|
+
RETIRED_MODES = {
|
|
154
|
+
"diff": "use `git diff` or the edit ledger",
|
|
155
|
+
"bug_scan": "use c3_search(action='exact', query='except ')",
|
|
156
|
+
"dense_map": "use mode='map'",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def _sweep_retired_caches(self) -> int:
|
|
160
|
+
"""Delete the pre-2.122.0 diff caches: one full copy of every file
|
|
161
|
+
ever diffed, keyed by BASENAME (so every __init__.py collided).
|
|
162
|
+
Cheap — only diff wrote `*.cache`; after one sweep there are none."""
|
|
163
|
+
removed = 0
|
|
164
|
+
try:
|
|
165
|
+
for f in self.cache_dir.glob("*.cache"):
|
|
166
|
+
try:
|
|
167
|
+
f.unlink()
|
|
168
|
+
removed += 1
|
|
169
|
+
except OSError:
|
|
170
|
+
continue
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
return removed
|
|
148
174
|
|
|
149
175
|
@staticmethod
|
|
150
176
|
def _normalize_rel_path(path: str) -> str:
|
|
@@ -192,13 +218,17 @@ class CodeCompressor:
|
|
|
192
218
|
Compress a source file.
|
|
193
219
|
|
|
194
220
|
Modes:
|
|
221
|
+
- "map": the canonical file map (services/file_map.render_map)
|
|
195
222
|
- "structure": Function/class signatures + imports (most compressed)
|
|
196
223
|
- "outline": Structure + docstrings + key comments
|
|
197
224
|
- "smart": Adaptive - more detail for small files, less for large
|
|
198
|
-
- "diff": Only changes since last seen (requires prior state)
|
|
199
225
|
- "summary": High-level LLM summary (requires router)
|
|
200
|
-
|
|
226
|
+
Retired (2.122.0): "diff", "bug_scan", "dense_map" -> error dict.
|
|
201
227
|
"""
|
|
228
|
+
if mode in self.RETIRED_MODES:
|
|
229
|
+
return {"error": f"mode '{mode}' retired in 2.122.0 — "
|
|
230
|
+
f"{self.RETIRED_MODES[mode]} (docs/file-map.md)",
|
|
231
|
+
"compressed": "", "retired": True}
|
|
202
232
|
# Access Guard: read verdict before any cache or disk access — denied
|
|
203
233
|
# paths raise AccessDenied (docs/access-guard.md §3). Callers that
|
|
204
234
|
# need string returns catch it and surface exc.message. Checked
|
|
@@ -241,8 +271,9 @@ class CodeCompressor:
|
|
|
241
271
|
content_hash = hashlib.md5(content.encode()).hexdigest()
|
|
242
272
|
ext = filepath.suffix.lower()
|
|
243
273
|
|
|
244
|
-
# Check persistent cache (
|
|
245
|
-
|
|
274
|
+
# Check persistent cache (summary has its own logic; map is served
|
|
275
|
+
# from file_memory's own cache below)
|
|
276
|
+
if mode not in ("summary", "map"):
|
|
246
277
|
cache_key = f"{content_hash}_{mode}{ext}.json"
|
|
247
278
|
if mask_view is not None:
|
|
248
279
|
cache_key = f"mask-{mask_view.view_hash}_{cache_key}"
|
|
@@ -271,7 +302,10 @@ class CodeCompressor:
|
|
|
271
302
|
# entries (keyed on content hash) still win. diff/summary are exempt:
|
|
272
303
|
# diff is the recommended targeted alternative and summary delegates
|
|
273
304
|
# to the router.
|
|
274
|
-
if mode
|
|
305
|
+
if mode == "map":
|
|
306
|
+
return self._canonical_map(filepath, content)
|
|
307
|
+
|
|
308
|
+
if mode != "summary":
|
|
275
309
|
line_count = content.count("\n")
|
|
276
310
|
if content and not content.endswith("\n"):
|
|
277
311
|
line_count += 1
|
|
@@ -285,9 +319,6 @@ class CodeCompressor:
|
|
|
285
319
|
filepath, content, content_hash, ext, mode,
|
|
286
320
|
byte_size, line_count)
|
|
287
321
|
|
|
288
|
-
if mode == "diff":
|
|
289
|
-
return self._diff_compress(filepath, content)
|
|
290
|
-
|
|
291
322
|
if mode == "summary":
|
|
292
323
|
if not self.router:
|
|
293
324
|
return {"error": "Summary mode requires a router", "compressed": ""}
|
|
@@ -296,31 +327,6 @@ class CodeCompressor:
|
|
|
296
327
|
result = f"# {filepath.name} — SUMMARY\n{summary}"
|
|
297
328
|
return {"compressed": result, "mode": "summary", **measure_savings(content, result)}
|
|
298
329
|
|
|
299
|
-
if mode == "bug_scan":
|
|
300
|
-
# Structure map + exception-handling annotation pass
|
|
301
|
-
structure = self._extract_structure(content, ext, "outline")
|
|
302
|
-
exception_section = self._scan_exception_handlers(content)
|
|
303
|
-
compressed_parts = [structure]
|
|
304
|
-
if exception_section:
|
|
305
|
-
compressed_parts.append(exception_section)
|
|
306
|
-
compressed = "\n".join(compressed_parts)
|
|
307
|
-
header = f"# {filepath.name} ({filepath.suffix}) — {len(content.splitlines())} lines [bug_scan]\n"
|
|
308
|
-
result = header + compressed
|
|
309
|
-
savings = measure_savings(content, result)
|
|
310
|
-
savings["compressed"] = result
|
|
311
|
-
savings["mode"] = "bug_scan"
|
|
312
|
-
savings["filepath"] = str(filepath)
|
|
313
|
-
self._file_hashes[str(filepath)] = content_hash
|
|
314
|
-
cache_key = f"{content_hash}_bug_scan{ext}.json"
|
|
315
|
-
self._mem_cache[cache_key] = dict(savings)
|
|
316
|
-
cache_file = self.cache_dir / cache_key
|
|
317
|
-
try:
|
|
318
|
-
with open(cache_file, "w", encoding="utf-8") as f:
|
|
319
|
-
json.dump(savings, f, indent=2)
|
|
320
|
-
except Exception:
|
|
321
|
-
pass
|
|
322
|
-
return savings
|
|
323
|
-
|
|
324
330
|
if mode == "smart":
|
|
325
331
|
tokens = count_tokens(content)
|
|
326
332
|
if tokens < 80:
|
|
@@ -354,8 +360,8 @@ class CodeCompressor:
|
|
|
354
360
|
# Cache hash for diff mode
|
|
355
361
|
self._file_hashes[str(filepath)] = content_hash
|
|
356
362
|
|
|
357
|
-
# Persist to cache
|
|
358
|
-
if mode
|
|
363
|
+
# Persist to cache
|
|
364
|
+
if mode != "summary":
|
|
359
365
|
cache_key = f"{content_hash}_{mode}{ext}.json"
|
|
360
366
|
# Store in memory first (fast path for repeat access)
|
|
361
367
|
if len(self._mem_cache) >= self._MEM_CACHE_MAX:
|
|
@@ -648,51 +654,6 @@ class CodeCompressor:
|
|
|
648
654
|
|
|
649
655
|
return None
|
|
650
656
|
|
|
651
|
-
def _scan_exception_handlers(self, content: str) -> str:
|
|
652
|
-
"""Scan for exception-handling hotspots and return an annotated section.
|
|
653
|
-
|
|
654
|
-
Returns a formatted block listing every bare/broad except clause with:
|
|
655
|
-
- line number
|
|
656
|
-
- the except line itself
|
|
657
|
-
- the immediately enclosing function name (if detectable)
|
|
658
|
-
|
|
659
|
-
Returns an empty string if no exception handlers are found.
|
|
660
|
-
"""
|
|
661
|
-
lines = content.splitlines()
|
|
662
|
-
# Patterns ranked from most to least problematic
|
|
663
|
-
_EXCEPT_PATTERNS = [
|
|
664
|
-
(re.compile(r"^\s*except\s*:"), "bare-except"),
|
|
665
|
-
(re.compile(r"^\s*except\s+Exception\s*:"), "broad-except"),
|
|
666
|
-
(re.compile(r"^\s*except\s+Exception\s+as\s+\w+\s*:"), "broad-except"),
|
|
667
|
-
(re.compile(r"^\s*except\s+\("), "multi-except"),
|
|
668
|
-
]
|
|
669
|
-
_FUNC_DEF = re.compile(r"^\s*(?:async\s+)?def\s+(\w+)")
|
|
670
|
-
|
|
671
|
-
hits: list[str] = []
|
|
672
|
-
# Track the most recently seen function name for context
|
|
673
|
-
current_func = "<module>"
|
|
674
|
-
for idx, line in enumerate(lines, start=1):
|
|
675
|
-
m = _FUNC_DEF.match(line)
|
|
676
|
-
if m:
|
|
677
|
-
current_func = m.group(1)
|
|
678
|
-
for pattern, label in _EXCEPT_PATTERNS:
|
|
679
|
-
if pattern.match(line):
|
|
680
|
-
# Show up to 2 continuation lines (body of the except block)
|
|
681
|
-
body_lines = []
|
|
682
|
-
for j in range(idx, min(idx + 2, len(lines))):
|
|
683
|
-
body = lines[j].strip()
|
|
684
|
-
if body and not body.startswith("except") and not body.startswith("try"):
|
|
685
|
-
body_lines.append(body)
|
|
686
|
-
body_preview = " | ".join(body_lines[:2]) if body_lines else ""
|
|
687
|
-
suffix = f" → {body_preview}" if body_preview else ""
|
|
688
|
-
hits.append(f" L{idx} [{label}] in `{current_func}`: {line.strip()}{suffix}")
|
|
689
|
-
break # one label per line
|
|
690
|
-
|
|
691
|
-
if not hits:
|
|
692
|
-
return ""
|
|
693
|
-
header = f"\n# Exception-handling hotspots ({len(hits)} found):"
|
|
694
|
-
return header + "\n" + "\n".join(hits)
|
|
695
|
-
|
|
696
657
|
def _generic_compress(self, content: str, ext: str) -> str:
|
|
697
658
|
"""Fallback compression for unknown languages."""
|
|
698
659
|
lines = content.split('\n')
|
|
@@ -712,49 +673,33 @@ class CodeCompressor:
|
|
|
712
673
|
kept.append(stripped)
|
|
713
674
|
return '\n'.join(kept)
|
|
714
675
|
|
|
715
|
-
def
|
|
716
|
-
"""
|
|
717
|
-
cache_file = self.cache_dir / f"{filepath.name}.cache"
|
|
718
|
-
current_hash = hashlib.md5(current_content.encode()).hexdigest()
|
|
676
|
+
def _canonical_map(self, filepath: Path, content: str) -> dict:
|
|
677
|
+
"""mode="map": the same text c3_read / c3_compress serve.
|
|
719
678
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
# Update cache
|
|
743
|
-
cache_file.write_text(current_content, encoding="utf-8")
|
|
679
|
+
Built from a throwaway FileMemoryStore-style extraction so callers
|
|
680
|
+
that only hold a compressor (Hub REST, `c3 compress`, delegate)
|
|
681
|
+
get the canonical map rather than a structure-only summary.
|
|
682
|
+
"""
|
|
683
|
+
from services.file_map import render_map
|
|
684
|
+
from services.file_memory import LANG_MAP, FileMemoryStore
|
|
685
|
+
ext = filepath.suffix.lower()
|
|
686
|
+
sections, parser = FileMemoryStore._extract_sections_with_parser(
|
|
687
|
+
FileMemoryStore.__new__(FileMemoryStore), filepath, content)
|
|
688
|
+
record = {
|
|
689
|
+
"path": self._relative_to_project(filepath),
|
|
690
|
+
"lines": len(content.splitlines()),
|
|
691
|
+
"language": LANG_MAP.get(ext, ext.lstrip(".")),
|
|
692
|
+
"parser": parser,
|
|
693
|
+
"sections": sections,
|
|
694
|
+
}
|
|
695
|
+
result = render_map(record)
|
|
696
|
+
savings = measure_savings(content, result)
|
|
697
|
+
savings["compressed"] = result
|
|
698
|
+
savings["mode"] = "map"
|
|
699
|
+
savings["parser"] = parser
|
|
700
|
+
savings["filepath"] = str(filepath)
|
|
744
701
|
return savings
|
|
745
702
|
|
|
746
|
-
def _contextual_diff(self, old_lines: list, new_lines: list, filename: str) -> str:
|
|
747
|
-
"""Generate a contextual diff with surrounding structure."""
|
|
748
|
-
import difflib
|
|
749
|
-
differ = difflib.unified_diff(old_lines, new_lines, lineterm='', n=1)
|
|
750
|
-
diff_text = '\n'.join(differ)
|
|
751
|
-
|
|
752
|
-
if not diff_text.strip():
|
|
753
|
-
return f"# {filename} — NO CHANGES"
|
|
754
|
-
|
|
755
|
-
header = f"# {filename} — CHANGES ONLY\n"
|
|
756
|
-
return header + diff_text
|
|
757
|
-
|
|
758
703
|
def compress_directory(self, dirpath: str, mode: str = "smart",
|
|
759
704
|
extensions: Optional[list] = None,
|
|
760
705
|
max_files: int = 50) -> dict:
|
services/dir_map.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Directory maps — one line per file, budgeted, for `c3_read('<dir>')`.
|
|
2
|
+
|
|
3
|
+
# services/ (58 files, 41,203L)
|
|
4
|
+
compressor.py (809L python) — CodeCompressor; compress_file; STRUCTURE_PATTERNS
|
|
5
|
+
file_map.py (280L python) — render_map; parse_signature; parse_map; KINDS
|
|
6
|
+
…
|
|
7
|
+
… 12 more files
|
|
8
|
+
|
|
9
|
+
Files are ranked the way a reader would want them: recently edited first
|
|
10
|
+
(the edit ledger), then by how much structure they hold (symbol count),
|
|
11
|
+
then by path. The map is filled to a token budget and closes with how
|
|
12
|
+
many files it left out. Traversal is bounded — the scanner's SKIP_DIRS
|
|
13
|
+
and .gitignore pruning, never following symlinks, a hard file cap — and
|
|
14
|
+
structure comes from file_memory records, extracting on the fly only
|
|
15
|
+
for a bounded number of code files (docs/file-map.md § Directories).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from core import count_tokens
|
|
24
|
+
from services.file_memory import CODE_EXTENSIONS, LANG_MAP
|
|
25
|
+
from services.scanner import iter_files
|
|
26
|
+
|
|
27
|
+
DEFAULT_MAX_TOKENS = 1500
|
|
28
|
+
MAX_FILES = 400 # hard traversal cap
|
|
29
|
+
MAX_EXTRACT_FILES = 120 # code files parsed on the fly, per call
|
|
30
|
+
EXTRACT_DEADLINE_S = 3.0 # then the rest show line counts only
|
|
31
|
+
MAX_EXTRACT_BYTES = 200 * 1024
|
|
32
|
+
SYMBOLS_PER_FILE = 6
|
|
33
|
+
LEDGER_LOOKBACK = 300 # recent ledger entries consulted for ranking
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _recent_edit_ranks(svc) -> dict:
|
|
37
|
+
"""path -> recency rank (0 = most recent) from the edit ledger."""
|
|
38
|
+
ledger = getattr(svc, "edit_ledger", None)
|
|
39
|
+
if ledger is None:
|
|
40
|
+
return {}
|
|
41
|
+
try:
|
|
42
|
+
entries = ledger.get_history(limit=LEDGER_LOOKBACK)
|
|
43
|
+
except Exception:
|
|
44
|
+
return {}
|
|
45
|
+
ranks: dict = {}
|
|
46
|
+
for entry in entries or []:
|
|
47
|
+
f = str((entry or {}).get("file") or "").replace("\\", "/")
|
|
48
|
+
if f and f not in ranks:
|
|
49
|
+
ranks[f] = len(ranks)
|
|
50
|
+
return ranks
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
#: What a reader wants to see first on a one-line summary of a file.
|
|
54
|
+
_SYMBOL_PRIORITY = {"class": 0, "interface": 0, "struct": 0, "trait": 0, "enum": 0,
|
|
55
|
+
"function": 1, "method": 1, "type": 2, "heading": 2, "section": 2,
|
|
56
|
+
"constant": 3, "variable": 3}
|
|
57
|
+
_SKIP_SYMBOL_TYPES = {"import", "decorator", "comment", "content", "property", "impl"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _structural_sections(record: Optional[dict]) -> list:
|
|
61
|
+
return [s for s in (record or {}).get("sections") or []
|
|
62
|
+
if s.get("type") not in _SKIP_SYMBOL_TYPES
|
|
63
|
+
and str(s.get("name") or "").strip() not in ("", "(full file)")]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _top_symbols(record: Optional[dict]) -> list:
|
|
67
|
+
"""Up to SYMBOLS_PER_FILE names: classes first, then functions, then the
|
|
68
|
+
rest, constants last — each group in source order."""
|
|
69
|
+
picked = sorted(enumerate(_structural_sections(record)),
|
|
70
|
+
key=lambda iv: (_SYMBOL_PRIORITY.get(iv[1].get("type"), 2),
|
|
71
|
+
str(iv[1].get("name") or "").startswith("_"),
|
|
72
|
+
iv[0]))
|
|
73
|
+
names = []
|
|
74
|
+
for _, s in picked[:SYMBOLS_PER_FILE]:
|
|
75
|
+
name = str(s.get("name") or "").strip()
|
|
76
|
+
if s.get("type") == "heading":
|
|
77
|
+
name = name.split(":", 1)[-1].strip()
|
|
78
|
+
names.append(name)
|
|
79
|
+
return names
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _line_count(path: Path) -> int:
|
|
83
|
+
try:
|
|
84
|
+
with open(path, "rb") as fh:
|
|
85
|
+
return sum(1 for _ in fh)
|
|
86
|
+
except OSError:
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def render_directory_map(svc, rel_dir: str, *, max_tokens: int = DEFAULT_MAX_TOKENS) -> tuple:
|
|
91
|
+
"""Return (text, detail) for the directory `rel_dir` (project-relative, posix)."""
|
|
92
|
+
root = Path(svc.project_path).resolve()
|
|
93
|
+
rel_dir = str(rel_dir).replace("\\", "/").strip("/")
|
|
94
|
+
target = (root / rel_dir).resolve() if rel_dir else root
|
|
95
|
+
label = (rel_dir + "/") if rel_dir else "./"
|
|
96
|
+
|
|
97
|
+
files = []
|
|
98
|
+
capped = False
|
|
99
|
+
for fp in iter_files(target, max_files=MAX_FILES + 1):
|
|
100
|
+
if len(files) >= MAX_FILES:
|
|
101
|
+
capped = True
|
|
102
|
+
break
|
|
103
|
+
try:
|
|
104
|
+
files.append(fp.resolve().relative_to(root).as_posix())
|
|
105
|
+
except ValueError:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
store = getattr(svc, "file_memory", None)
|
|
109
|
+
recent = _recent_edit_ranks(svc)
|
|
110
|
+
started = time.monotonic()
|
|
111
|
+
extracted = 0
|
|
112
|
+
rows = []
|
|
113
|
+
total_lines = 0
|
|
114
|
+
for rel in files:
|
|
115
|
+
record = None
|
|
116
|
+
if store is not None:
|
|
117
|
+
try:
|
|
118
|
+
record = store.get(rel)
|
|
119
|
+
except Exception:
|
|
120
|
+
record = None
|
|
121
|
+
ext = Path(rel).suffix.lower()
|
|
122
|
+
if (record is None and ext in CODE_EXTENSIONS
|
|
123
|
+
and extracted < MAX_EXTRACT_FILES
|
|
124
|
+
and time.monotonic() - started < EXTRACT_DEADLINE_S):
|
|
125
|
+
try:
|
|
126
|
+
if (root / rel).stat().st_size <= MAX_EXTRACT_BYTES:
|
|
127
|
+
record = store.update(rel)
|
|
128
|
+
extracted += 1
|
|
129
|
+
except Exception:
|
|
130
|
+
record = None
|
|
131
|
+
lines = int((record or {}).get("lines") or 0) or _line_count(root / rel)
|
|
132
|
+
total_lines += lines
|
|
133
|
+
ext = Path(rel).suffix.lower()
|
|
134
|
+
lang = str((record or {}).get("language") or LANG_MAP.get(ext) or ext.lstrip(".") or "")
|
|
135
|
+
symbols = _top_symbols(record)
|
|
136
|
+
rows.append({
|
|
137
|
+
"rel": rel,
|
|
138
|
+
"name": rel[len(rel_dir) + 1:] if rel_dir and rel.startswith(rel_dir + "/") else rel,
|
|
139
|
+
"lines": lines,
|
|
140
|
+
"lang": lang,
|
|
141
|
+
"symbols": symbols,
|
|
142
|
+
"recent": recent.get(rel, 10 ** 6),
|
|
143
|
+
"nsym": len(_structural_sections(record)),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
rows.sort(key=lambda r: (r["recent"], -r["nsym"], r["rel"]))
|
|
147
|
+
|
|
148
|
+
header = f"# {label} ({len(files)}{'+' if capped else ''} files, {total_lines:,}L)"
|
|
149
|
+
body = []
|
|
150
|
+
for r in rows:
|
|
151
|
+
line = f"{r['name']} ({r['lines']}L {r['lang']})".rstrip()
|
|
152
|
+
if r["symbols"]:
|
|
153
|
+
line += " — " + "; ".join(r["symbols"])
|
|
154
|
+
body.append(line)
|
|
155
|
+
|
|
156
|
+
kept = list(body)
|
|
157
|
+
while kept and count_tokens("\n".join([header] + kept)) > max_tokens:
|
|
158
|
+
kept.pop()
|
|
159
|
+
dropped = len(body) - len(kept)
|
|
160
|
+
parts = [header] + kept
|
|
161
|
+
if dropped:
|
|
162
|
+
parts.append(f"… {dropped} more files")
|
|
163
|
+
if capped:
|
|
164
|
+
parts.append(f"[dir_map] traversal capped at {MAX_FILES} files; map a subdirectory")
|
|
165
|
+
# In source order the reader gets recency first; say so once.
|
|
166
|
+
parts.append("[dir_map] recently edited first, then by structure; "
|
|
167
|
+
"c3_read('<file>') maps one file")
|
|
168
|
+
text = "\n".join(parts)
|
|
169
|
+
detail = {
|
|
170
|
+
"backend": "directory",
|
|
171
|
+
"files": len(files),
|
|
172
|
+
"listed": len(kept),
|
|
173
|
+
"extracted": extracted,
|
|
174
|
+
"capped": capped,
|
|
175
|
+
}
|
|
176
|
+
return text, detail
|
|
File without changes
|
{code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.121.0.dist-info → code_context_control-2.123.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|