Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,49 @@ All notable changes to Code Context Control (C3) are documented here.
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.120.0] - 2026-09-06

### Added — every file map is measured (c3_compress remediation, C0)

Measured on this repository's telemetry (3,932 tool calls, Jul–Sep 2026):
`c3_compress` was called 132 times, 130 of them `mode='map'`, and its rows
never said which backend built the map, which parser extracted the
symbols, whether the record was already fresh, or whether the map led to
a targeted read. This release adds the instrument before the renderer
changes (2.121.0) so the change can be graded.

- `file_memory` records carry `parser` (`tree_sitter | regex | generic`);
a pre-2.120.0 record without it is re-extracted on its next update.
- `c3_compress` and `c3_read` rows carry a flat `detail`: requested mode,
backend (`file_memory`, `compressor`, `large_fast_path`, `batch`,
`source`), parser, `cache_hit`, section count, symbols requested, and
why a read served a map (`map_only`, `symbols_not_found`) or how much
source it served (`ranges`, `lines_served`, `file_lines`).
- `aggregate_tool_telemetry` folds them into `map_by_backend` (calls,
tokens, cache hits, ratio p50/p95 per backend/parser) and
`map_read_chain` (maps followed by a read of the same file within five
calls, reads preceded by a map) — the before/after instrument for the
remediation. The chain works on `target` alone, so rows written since
2.111.0 count.
- `c3 map-eval`: a hand-annotated gold harness for the file map
(`services/bench/map_eval.py`, `tests/map_eval/`), mirroring
`c3 shell-eval`: 16 fixtures across Python, TypeScript, JavaScript, Go,
Rust, Markdown, YAML, JSON, CSS, HTML, R (regex fallback), Java
(generic), a malformed file, a minified bundle and a 250 KB generated
file. Each case scores tokens, symbol recall/precision, signature
completeness, range accuracy and determinism against the annotation —
not against tree-sitter, which would be circular. Baseline committed in
`tests/map_eval/baseline_fixture.json` (docs/map-eval.md).

### Measured on the baseline (what 2.121.0 fixes)

Signature completeness 0.20, symbol recall 0.72, 14% of map characters are
emoji and padding; multi-line Python signatures print with newlines; YAML
files that start with a comment render no keys; HTML ids, CSS media
queries, Go const blocks and receivers, Rust impl methods and nested-class
methods are missing; a 20 KB minified line becomes 549 symbols and an
11 MB record.

## [2.119.0] - 2026-09-05

### Added — native Codex integration: hooks, thread-bound handoffs, additive installs
Expand Down
39 changes: 38 additions & 1 deletion cli/c3.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
# Config
CONFIG_DIR = ".c3"
CONFIG_FILE = ".c3/config.json"
__version__ = "2.119.0"
__version__ = "2.120.0"


def _compress_file_cli(compressor, path, mode="smart", **kw):
Expand Down Expand Up @@ -1570,6 +1570,42 @@ def cmd_shell_eval(args):
sys.exit(1)


def cmd_map_eval(args):
"""Run the file-map gold suite and compare it to its baseline.

Renders the hand-annotated fixtures through the renderer the MCP tool
uses and grades symbol recall, signatures, ranges and determinism. Exit
status 1 when the baseline is violated (a must_pass case failed, an
aggregate fell under its floor or rose over its ceiling, a complete map
grew). See docs/map-eval.md.
"""
from services.bench import map_eval as me

try:
report = me.run_suite(args.suite, baseline=args.baseline)
except FileNotFoundError as e:
raise RuntimeError(str(e))

if args.update_baseline:
path = args.baseline or me.BUNDLED_BASELINES.get(report.suite)
if not path:
raise RuntimeError(
f"suite {report.suite!r} has no bundled baseline; pass --baseline PATH")
floors = json.loads(args.floors) if args.floors else None
ceilings = json.loads(args.ceilings) if args.ceilings else None
me.write_baseline(report, path, floors=floors, ceilings=ceilings)
report.baseline_violations, report.baseline_warnings = me.compare_to_baseline(
report, me.load_baseline(path))
print(f"baseline written: {path}")

if args.json:
print(json.dumps(report.to_dict(), indent=2))
else:
print(report.render())
if report.baseline_violations:
sys.exit(1)


def _benchmark_extract_preview(full_path: Path, compressor: CodeCompressor, pattern: str = "", max_lines: int = 50) -> str:
"""Approximate c3_filter behavior for local benchmarking without MCP startup."""
import re as _re
Expand Down Expand Up @@ -9261,6 +9297,7 @@ def main():
"stats": cmd_stats,
"search-eval": cmd_search_eval,
"shell-eval": cmd_shell_eval,
"map-eval": cmd_map_eval,
"benchmark": cmd_benchmark,
"session-benchmark": cmd_session_benchmark,
"benchmark-e2e": cmd_benchmark_e2e,
Expand Down
13 changes: 13 additions & 0 deletions cli/commands/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ def build_parser(version: str, parse_cli_ide_arg):
help='With --update-baseline: JSON object of metric ceilings, e.g. \'{"bytes_p95": 18432}\'')
p_shell_eval.add_argument("--json", action="store_true", help="Emit the full report as JSON")

p_map_eval = subparsers.add_parser(
"map-eval", help="Run the file-map gold suite (docs/map-eval.md)")
p_map_eval.add_argument("--suite", default="fixture",
help="'fixture' (hand-annotated sources, CI gate) or a .jsonl path")
p_map_eval.add_argument("--baseline", default=None, help="Baseline JSON to compare against / write")
p_map_eval.add_argument("--update-baseline", action="store_true",
help="Write aggregates + per-case status to the baseline (floors/ceilings are kept)")
p_map_eval.add_argument("--floors", default=None,
help='With --update-baseline: JSON object of metric floors, e.g. \'{"symbol_recall_mean": 0.9}\'')
p_map_eval.add_argument("--ceilings", default=None,
help='With --update-baseline: JSON object of metric ceilings, e.g. \'{"tokens_p95": 4000}\'')
p_map_eval.add_argument("--json", action="store_true", help="Emit the full report as JSON")

p_compress = subparsers.add_parser("compress", help="Compress a file")
p_compress.add_argument("file", help="File to compress")
p_compress.add_argument("--mode", choices=["map", "dense_map", "smart", "diff"], default="smart")
Expand Down
54 changes: 51 additions & 3 deletions cli/tools/compress.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,48 @@
from services import access_guard


def map_detail(svc, rel: str, requested_mode: str, *, cache_hit: bool,
symbols: int = 0, fallback: str = "") -> dict:
"""Telemetry `detail` for a map served from file_memory (C0, 2.120.0).

backend names the code path, parser the extractor that built the record
(tree_sitter | regex | generic, "" for a pre-2.120.0 record), cache_hit
whether the record was already fresh. `symbols` is how many the caller
asked for and `fallback` why a read served a map instead of source.
Flat so telemetry.aggregate_tool_telemetry can fold it without a schema.
"""
record = None
try:
record = svc.file_memory.get(rel)
except Exception:
record = None
if not isinstance(record, dict):
record = None
detail = {
"requested_mode": requested_mode,
"backend": "file_memory",
"parser": str((record or {}).get("parser") or ""),
"cache_hit": bool(cache_hit),
"sections": len((record or {}).get("sections") or []),
}
if symbols:
detail["symbols"] = int(symbols)
if fallback:
detail["fallback"] = fallback
return detail


def compressor_detail(res: dict, requested_mode: str) -> dict:
"""Telemetry `detail` for a CodeCompressor result (smart/diff/bug_scan…)."""
backend = "large_fast_path" if res.get("fast_path") else "compressor"
return {
"requested_mode": requested_mode,
"backend": backend,
"actual_mode": str(res.get("mode") or ""),
"cache_hit": bool(res.get("cache_hit")),
}


def _run_memory_mcp_cli(args: list, cwd: str, timeout: int = 30) -> tuple:
"""Run codebase-memory-mcp CLI and return (success, output_or_error)."""
binary = shutil.which("codebase-memory-mcp")
Expand Down Expand Up @@ -150,6 +192,8 @@ def _compress_single(file_path: str, mode: str, svc, finalize, maybe_facts) -> s
return ("[file_map:error] not found. To create a new file use "
"c3_edit(file_path=..., old_string='', new_string=<content>).")
rel = str(full.resolve().relative_to(Path(svc.project_path).resolve())).replace("\\", "/")
# C0 measurement: was the record already fresh before this call?
cache_hit = not svc.file_memory.needs_update(rel)
queued = svc.file_memory.drain_queue()
completed = []
failed = []
Expand Down Expand Up @@ -183,7 +227,8 @@ def _compress_single(file_path: str, mode: str, svc, finalize, maybe_facts) -> s
finalize, svc, "c3_compress", {"file_path": file_path, "mode": mode},
res, summary,
raw_tokens=raw_tokens, optimized_tokens=map_tokens or None,
response_tokens=map_tokens)
response_tokens=map_tokens,
detail=map_detail(svc, rel, mode, cache_hit=cache_hit))

try:
res = svc.compressor.compress_file(str(full), mode)
Expand All @@ -197,7 +242,8 @@ def _compress_single(file_path: str, mode: str, svc, finalize, maybe_facts) -> s
finalize, svc, "c3_compress", {"file_path": file_path},
resp + maybe_facts(svc, Path(file_path).name), mode,
raw_tokens=res.get('original_tokens'),
optimized_tokens=res.get('compressed_tokens'))
optimized_tokens=res.get('compressed_tokens'),
detail=compressor_detail(res, mode))


def _compress_batch(paths: list, mode: str, svc, finalize, maybe_facts) -> str:
Expand Down Expand Up @@ -288,4 +334,6 @@ def _do_one(fp):
{"file_path": ",".join(paths), "mode": mode, "batch": True},
body, f"batch {total_ok}/{len(paths)}",
raw_tokens=total_raw if measured else None,
optimized_tokens=total_opt if measured else None)
optimized_tokens=total_opt if measured else None,
detail={"requested_mode": mode, "backend": "batch",
"files": len(paths), "ok": total_ok})
22 changes: 19 additions & 3 deletions cli/tools/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from cli.tools import _grants
from cli.tools._helpers import finalize_with_tokens, maybe_related_facts
from cli.tools.compress import map_detail
from core import count_tokens
from services import access_guard

Expand Down Expand Up @@ -301,15 +302,19 @@ def _mark(secs):
ranges.extend(main_ranges)

if not ranges and symbols:
map_cached = not svc.file_memory.needs_update(rel_path)
file_map = svc.file_memory.get_or_build_map(rel_path)
resp = f"[read:{file_path}] symbols not found: {symbols}. Showing file map:\n{file_map}"
map_tok = count_tokens(file_map)
return finalize_with_tokens(
finalize, svc, "c3_read", {"file": file_path, "symbols": symbols},
resp, f"{full_file_tokens()}->{map_tok}tok",
raw_tokens=full_file_tokens(), optimized_tokens=map_tok)
raw_tokens=full_file_tokens(), optimized_tokens=map_tok,
detail=map_detail(svc, rel_path, "read", cache_hit=map_cached,
symbols=len(symbols), fallback="symbols_not_found"))

if not ranges:
map_cached = not svc.file_memory.needs_update(rel_path)
file_map = svc.file_memory.get_or_build_map(rel_path)
resp = (file_map
+ "\n[map only — pass lines=[start,end] or symbols=[...] for exact source]"
Expand All @@ -319,7 +324,9 @@ def _mark(secs):
finalize, svc, "c3_read", {"file": file_path},
resp, f"{full_file_tokens()}->{map_tok}tok",
raw_tokens=full_file_tokens(), optimized_tokens=map_tok,
response_tokens=map_tok)
response_tokens=map_tok,
detail=map_detail(svc, rel_path, "read", cache_hit=map_cached,
fallback="map_only"))
else:
# Sort and merge overlapping ranges
ranges.sort()
Expand Down Expand Up @@ -359,7 +366,16 @@ def _mark(secs):
resp = final_content + maybe_related_facts(svc, rel_path, top_k=3, context="read")
tokens = count_tokens(resp)
summary = f"{full_file_tokens()}->{tokens}tok" if tokens < full_file_tokens() else f"{tokens}tok"
# C0 measurement: what the read asked for and how much source it took.
read_detail = {
"backend": "source",
"symbols": len(symbols) if symbols else 0,
"by_lines": bool(lines),
"ranges": len(ranges),
"lines_served": sum(e - s_ + 1 for s_, e in ranges),
"file_lines": len(content_lines),
}
return finalize_with_tokens(
finalize, svc, "c3_read", {"file": file_path, "symbols": symbols}, resp, summary,
raw_tokens=full_file_tokens(), optimized_tokens=tokens,
response_tokens=tokens)
response_tokens=tokens, detail=read_detail)
Loading
Loading