code-context-control 2.121.0__py3-none-any.whl → 2.122.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 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.121.0"
95
+ __version__ = "2.122.0"
96
96
 
97
97
 
98
98
  def _compress_file_cli(compressor, path, mode="smart", **kw):
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 "smart"
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", "dense_map", "smart", "diff"], default="smart")
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/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='map' (default) is the same text c3_read(file_path) serves. The other
4
- modes (dense_map, smart, diff, bug_scan, ast) are legacy and retire in 2.122.0."""
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
- # Validate mode
141
- valid_modes = ("map", "dense_map", "smart", "diff", "bug_scan", "ast")
142
- if mode == "ast":
143
- return _compress_ast(file_path, svc, finalize, maybe_facts)
144
- if mode not in valid_modes:
145
- # Graceful migration for removed modes
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, mode, svc, finalize, maybe_facts)
83
+ return _compress_batch(paths, requested, note, svc, finalize)
84
+
85
+ return _compress_single(file_path, requested, note, svc, finalize)
157
86
 
158
- return _compress_single(file_path, mode, svc, finalize, maybe_facts)
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, mode: str, svc, finalize, maybe_facts) -> str:
162
- """Compress a single file."""
163
- # Access Guard: read verdict up front — covers the map/dense_map paths
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": 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/dense_map modes build their output through
173
- # file_memory, which reads the RAW file — so a masked path must never
174
- # reach them. Other modes fall through to compressor.compress_file, which
175
- # compresses the materialized view (docs/mask-guard.md §6, row 7).
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": 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": 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
- if mode in ("map", "dense_map"):
193
- if not full.exists():
194
- return ("[file_map:error] not found. To create a new file use "
195
- "c3_edit(file_path=..., old_string='', new_string=<content>).")
196
- rel = str(full.resolve().relative_to(Path(svc.project_path).resolve())).replace("\\", "/")
197
- # C0 measurement: was the record already fresh before this call?
198
- cache_hit = not svc.file_memory.needs_update(rel)
199
- queued = svc.file_memory.drain_queue()
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
- res = svc.compressor.compress_file(str(full), mode)
243
- except access_guard.AccessDenied as exc:
244
- return finalize("c3_compress", {"file_path": file_path, "mode": mode},
245
- exc.message, "access-denied")
246
- if "error" in res:
247
- return f"Error: {res['error']}"
248
- resp = res['compressed']
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
- resp + maybe_facts(svc, Path(file_path).name), mode,
252
- raw_tokens=res.get('original_tokens'),
253
- optimized_tokens=res.get('compressed_tokens'),
254
- detail=compressor_detail(res, mode))
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, mode: str, svc, finalize, maybe_facts) -> str:
258
- """Compress multiple files in parallel, return combined report."""
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, compressed_text, raw_tokens, optimized_tokens, error)."""
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
- if mode in ("map", "dense_map"):
291
- rel = str(full.resolve().relative_to(
292
- Path(svc.project_path).resolve())).replace("\\", "/")
293
- res = (svc.file_memory.get_or_build_dense_map(rel)
294
- if mode == "dense_map"
295
- else svc.file_memory.get_or_build_map(rel))
296
- try:
297
- raw_tok = count_tokens(full.read_text(encoding="utf-8", errors="replace"))
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, compressed, raw_tok, opt_tok, err = fut.result()
315
- results[fp] = (compressed, raw_tok, opt_tok, err)
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
- compressed, raw_tok, opt_tok, err = results.get(fp, (None, None, None, "unknown"))
325
- if compressed:
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{compressed}")
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 ({mode})"
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": mode, "batch": True},
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={"requested_mode": mode, "backend": "batch",
347
- "files": len(paths), "ok": total_ok})
251
+ detail=detail)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.121.0
3
+ Version: 2.122.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=33z6xHxi3hdsM-zyi4WAM8QGW9rjFuuas5rpc357tMA,416811
4
+ cli/c3.py,sha256=DWhVK-bMa3_Tzz1cXdmjS0197VmE1b4Xb8HCtLa8-tw,416811
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
@@ -30,8 +30,8 @@ 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=zkxr0zW9KB600BlarJ2VXf7UOp2ak21R6uXZKdGvpyg,14413
34
- cli/commands/parser.py,sha256=d4nL6_UPeY4_Cb4ecAWjNyZBVvfdO450WFJavA2E9Fc,65353
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
@@ -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=3uvtjH1OWCzM5XKpfth7xRnJjlMxUq-LZkql8KG7SvQ,15362
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
@@ -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.121.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
126
+ code_context_control-2.122.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
@@ -203,7 +203,7 @@ services/circuit_breaker.py,sha256=ES6PpjL40gfDhtK88GeS6GFzsz5EOEEkZsB-DzX_tbw,3
203
203
  services/claude_md.py,sha256=jXvr0orZOkPYrVxEkf67beAj8EKPbB_M99mmxxMsPg4,61217
204
204
  services/codex_integration.py,sha256=dXlaVbQJrIrlZw5hZcDjhZVli3AvumnOTQTkl-_I-rg,7708
205
205
  services/codex_transcript.py,sha256=TtxKiNkZ3vsWpaQRczaIGsA7wd8r24z_65Nnso2BNj0,3658
206
- services/compressor.py,sha256=oX5Tnn42syY_08_SWukEkRamwTH7qwQ-T6TGIlZNgJY,36148
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
@@ -305,8 +305,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
305
305
  tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
306
306
  tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
307
307
  tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
308
- code_context_control-2.121.0.dist-info/METADATA,sha256=bLL-hxMFWw55dYOgBSwJqDy_Uq73ZXh1ZUmievzSMOU,27067
309
- code_context_control-2.121.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
310
- code_context_control-2.121.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
311
- code_context_control-2.121.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
312
- code_context_control-2.121.0.dist-info/RECORD,,
308
+ code_context_control-2.122.0.dist-info/METADATA,sha256=-kj7pKeDDSrFJ_FyQa-f4EelTWd5cOrlYl4lJWIu4GQ,27067
309
+ code_context_control-2.122.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
310
+ code_context_control-2.122.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
311
+ code_context_control-2.122.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
312
+ code_context_control-2.122.0.dist-info/RECORD,,
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
- - Function/class signatures only (skip bodies)
6
- - Structural outline mode
7
- - Smart truncation with context preservation
8
- - Diff-only mode for edits
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
- - "bug_scan": Structure map + annotated exception-handling hotspots with line numbers
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 (except for diff/summary which have their own logic)
245
- if mode not in ("diff", "summary"):
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 not in ("diff", "summary"):
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 (except diff which uses its own file format)
358
- if mode not in ("diff", "summary"):
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 _diff_compress(self, filepath: Path, current_content: str) -> dict:
716
- """Generate diff-based compression against cached version."""
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
- if cache_file.exists():
721
- cached = cache_file.read_text(encoding="utf-8", errors="replace")
722
- cached_hash = hashlib.md5(cached.encode()).hexdigest()
723
-
724
- if cached_hash == current_hash:
725
- result = f"# {filepath.name} — NO CHANGES"
726
- return {"compressed": result, "mode": "diff-unchanged", **measure_savings(current_content, result)}
727
-
728
- # Generate contextual diff
729
- diff = self._contextual_diff(cached.split('\n'), current_content.split('\n'), filepath.name)
730
- savings = measure_savings(current_content, diff)
731
- savings["compressed"] = diff
732
- savings["mode"] = "diff"
733
- else:
734
- # No cache — fall back to structure mode
735
- compressed = self._extract_structure(current_content, filepath.suffix.lower(), "structure")
736
- header = f"# {filepath.name} (FIRST SEEN) — {len(current_content.splitlines())} lines\n"
737
- result = header + compressed
738
- savings = measure_savings(current_content, result)
739
- savings["compressed"] = result
740
- savings["mode"] = "diff-first"
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: