Skip to content

Commit e1877fd

Browse files
author
jwmar
committed
Add behavior-surface retrieval and tool server
1 parent 8da93ee commit e1877fd

9 files changed

Lines changed: 1966 additions & 19 deletions

File tree

README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DeCodifier v3.1 - Developer Preview (Alpha)
22

3-
DeCodifier gets code agents to the right method, caller, and framework entrypoint first.
3+
DeCodifier gets code agents to the full behavioral change surface first.
44

55
DeCodifier is a local AI coding engine with deterministic method-first retrieval that lets LLMs
66
safely inspect and modify real projects. It provides the file operations, project registry, and
@@ -17,6 +17,8 @@ DeCodifier now exposes a deterministic retrieval layer for agent-friendly code l
1717
- `search_symbols(query)` for ranked method/class hits
1818
- `get_context_read_plan(query)` for bounded read planning
1919
- `materialize_context(plan)` for budgeted context rendering
20+
- behavior-surface bundles for entrypoints, callers, implementations, guards, dispatchers, REPLs, simulators, and bridges
21+
- per-hit rationale/debug metadata so agents can inspect why a symbol ranked where it did
2022

2123
Current three-repo benchmark snapshot:
2224

@@ -26,8 +28,16 @@ Current three-repo benchmark snapshot:
2628
| Embedding baseline | 36% | 69% | 28% |
2729
| Lexical baseline | 28% | 62% | 44% |
2830

31+
Current change-surface benchmark snapshot:
32+
33+
| System | Anchor Recall | Surface-Bundle Recall | Full Change-Set Rate | False Positives |
34+
| --- | ---: | ---: | ---: | ---: |
35+
| DeCodifier | 100% | 100% | 100% | 0% |
36+
| Embedding baseline | 85% | 0% | 20% | 28% |
37+
| Lexical baseline | 65% | 0% | 20% | 44% |
38+
2939
On the current three-repo benchmark suite, DeCodifier outperforms lexical and embedding baselines
30-
on precision, recall, caller/trace handling, and false-positive control.
40+
on precision, recall, caller/trace handling, full change-surface retrieval, and false-positive control.
3141

3242
### Codex Dogfood Run
3343

@@ -49,6 +59,23 @@ And benchmark the static fixture repos with DeCodifier plus the lexical and embe
4959
decodifier benchmark
5060
```
5161

62+
The benchmark now tracks change-oriented retrieval quality as well as first-hit accuracy, including
63+
anchor-set recall, surface-bundle recall, full change-surface success, and tokens to the full
64+
retrieval set.
65+
66+
For local agent integration without running the FastAPI server, you can expose the retrieval tools
67+
over stdio JSON:
68+
69+
```bash
70+
decodifier tool-server --path /path/to/repo
71+
```
72+
73+
Send newline-delimited requests like:
74+
75+
```json
76+
{"id":1,"tool":"search_symbols","arguments":{"query":"where are permissions checked","max_symbols":3}}
77+
```
78+
5279
## Quickstart
5380

5481
```bash
@@ -122,5 +149,3 @@ Not yet ready for:
122149

123150
This is the alpha. Expect rough edges.
124151
Open issues, PRs, crashes, and questions welcome.
125-
126-

decodifier/benchmark.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,24 @@ def _tokens_to_first_correct(context: Dict[str, Any], expected_symbols: Sequence
274274
return None
275275

276276

277+
def _tokens_to_full_change_surface(context: Dict[str, Any], expected_symbols: Sequence[str]) -> Optional[int]:
278+
if not expected_symbols:
279+
return None
280+
281+
expected_set = set(expected_symbols)
282+
seen: set[str] = set()
283+
token_total = 0
284+
for section in context.get("sections", []):
285+
token_total += int(section.get("token_count", retrieval._approx_token_count(section["content"])))
286+
if section.get("title") != "Primary":
287+
continue
288+
if section["symbol"] in expected_set:
289+
seen.add(section["symbol"])
290+
if seen >= expected_set:
291+
return token_total
292+
return None
293+
294+
277295
def _status_for_case(case: Dict[str, Any]) -> str:
278296
if case["query_type"] == "no_answer":
279297
return "ignored" if case["no_answer_correct"] else "false_positive"
@@ -294,11 +312,23 @@ def _summarize_cases(cases: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
294312
token_counts = [case["token_count"] for case in cases]
295313
retrieved_context_total = sum(case["retrieved_context_count"] for case in positive_cases)
296314
relevant_context_total = sum(case["relevant_context_count"] for case in positive_cases)
315+
anchor_set_recall_values = [case["anchor_set_recall"] for case in positive_cases if case["anchor_set_recall"] is not None]
316+
surface_bundle_recall_values = [
317+
case["surface_bundle_recall"]
318+
for case in positive_cases
319+
if case["surface_bundle_recall"] is not None
320+
]
297321
tokens_to_first_correct = [
298322
case["tokens_to_first_correct"]
299323
for case in positive_cases
300324
if case["tokens_to_first_correct"] is not None
301325
]
326+
multi_anchor_cases = [case for case in positive_cases if len(case["expected_symbols"]) > 1]
327+
tokens_to_full_change_surface = [
328+
case["tokens_to_full_change_surface"]
329+
for case in multi_anchor_cases
330+
if case["tokens_to_full_change_surface"] is not None
331+
]
302332

303333
return {
304334
"query_count": len(cases),
@@ -313,6 +343,17 @@ def _summarize_cases(cases: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
313343
),
314344
"false_positive_rate": _ratio(sum(1 for case in cases if case["false_positive"]), len(cases)),
315345
"average_tokens_to_first_correct": _mean(tokens_to_first_correct),
346+
"anchor_set_recall": round(mean(anchor_set_recall_values), 4) if anchor_set_recall_values else None,
347+
"surface_bundle_recall": round(mean(surface_bundle_recall_values), 4) if surface_bundle_recall_values else None,
348+
"full_change_surface_rate": _ratio(
349+
sum(1 for case in multi_anchor_cases if case["full_change_surface_found"]),
350+
len(multi_anchor_cases),
351+
),
352+
"paired_surface_top_hit": _ratio(
353+
sum(1 for case in multi_anchor_cases if case["paired_surface_top_hit"]),
354+
len(multi_anchor_cases),
355+
),
356+
"average_tokens_to_full_change_surface": _mean(tokens_to_full_change_surface),
316357
"top1_accuracy": _ratio(sum(1 for case in cases if case["top1_correct"]), len(cases)),
317358
"topk_accuracy": _ratio(sum(1 for case in cases if case["topk_exact"]), len(cases)),
318359
"definition_hit_rate": _ratio(
@@ -411,6 +452,20 @@ def _run_engine_case(
411452
no_answer_correct = expected_list == [] and context_symbols == []
412453
relevant_context_count = sum(1 for symbol in context_symbols if symbol in expected_set)
413454
retrieved_context_count = len(context_symbols)
455+
surface_bundle_symbols = [item["symbol"] for item in plan.get("surface_bundle", [])]
456+
surface_bundle_relevant_count = len(set(surface_bundle_symbols) & expected_set)
457+
anchor_set_recall = (
458+
round(relevant_context_count / len(expected_list), 4)
459+
if expected_list
460+
else None
461+
)
462+
surface_bundle_recall = (
463+
round(surface_bundle_relevant_count / len(expected_list), 4)
464+
if expected_list
465+
else None
466+
)
467+
full_change_surface_found = bool(expected_list) and all(symbol in context_symbols for symbol in expected_list)
468+
paired_surface_top_hit = bool(len(expected_list) > 1 and context_symbols[:1] == expected_list[:1] and expected_list[1] in context_symbols)
414469

415470
return {
416471
"query": query,
@@ -429,12 +484,18 @@ def _run_engine_case(
429484
if expected_list and retrieved_context_count
430485
else None
431486
),
487+
"anchor_set_recall": anchor_set_recall,
488+
"surface_bundle_recall": surface_bundle_recall,
489+
"full_change_surface_found": full_change_surface_found,
490+
"paired_surface_top_hit": paired_surface_top_hit,
432491
"tokens_to_first_correct": _tokens_to_first_correct(context, expected_list),
492+
"tokens_to_full_change_surface": _tokens_to_full_change_surface(context, expected_list),
433493
"false_positive": retrieved_context_count > 0 and relevant_context_count == 0,
434494
"token_count": context["token_count"],
435495
"line_count": context["line_count"],
436496
"truncated": context["truncated"],
437497
"plan_symbols": [entry["symbol"] for entry in plan.get("entries", [])],
498+
"surface_bundle_symbols": surface_bundle_symbols,
438499
}
439500

440501

@@ -530,21 +591,26 @@ def render_fixture_benchmark_markdown(report: Dict[str, Any]) -> str:
530591
"",
531592
"## Aggregate",
532593
"",
533-
"| Engine | Budget | Queries | Precision | Recall | False positives | Tokens to first correct | Top-1 | Top-K | Caller | Trace | No-answer | Avg context tokens |",
534-
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
594+
"| Engine | Budget | Queries | Precision | Recall | Anchor recall | Surface bundle | False positives | Tokens to first correct | Tokens to full set | Full change set | Paired top | Top-1 | Top-K | Caller | Trace | No-answer | Avg context tokens |",
595+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
535596
]
536597
for engine_name in report["config"]["engines"]:
537598
for budget in report["config"]["token_budgets"]:
538599
summary = report["engines"][engine_name]["budgets"][str(budget)]["summary"]
539600
lines.append(
540-
"| {engine} | {budget} | {query_count} | {precision} | {recall} | {false_positive} | {first_correct_tokens} | {top1} | {topk} | {caller} | {trace} | {no_answer} | {tokens} |".format(
601+
"| {engine} | {budget} | {query_count} | {precision} | {recall} | {anchor_recall} | {bundle_recall} | {false_positive} | {first_correct_tokens} | {full_surface_tokens} | {full_surface_rate} | {paired_top} | {top1} | {topk} | {caller} | {trace} | {no_answer} | {tokens} |".format(
541602
engine=ENGINE_LABELS.get(engine_name, engine_name),
542603
budget=budget,
543604
query_count=summary["query_count"],
544605
precision=_format_metric(summary["context_precision"], percent=True),
545606
recall=_format_metric(summary["context_recall"], percent=True),
607+
anchor_recall=_format_metric(summary["anchor_set_recall"], percent=True),
608+
bundle_recall=_format_metric(summary["surface_bundle_recall"], percent=True),
546609
false_positive=_format_metric(summary["false_positive_rate"], percent=True),
547610
first_correct_tokens=_format_metric(summary["average_tokens_to_first_correct"]),
611+
full_surface_tokens=_format_metric(summary["average_tokens_to_full_change_surface"]),
612+
full_surface_rate=_format_metric(summary["full_change_surface_rate"], percent=True),
613+
paired_top=_format_metric(summary["paired_surface_top_hit"], percent=True),
548614
top1=_format_metric(summary["top1_accuracy"], percent=True),
549615
topk=_format_metric(summary["topk_accuracy"], percent=True),
550616
caller=_format_metric(summary["caller_hit_rate"], percent=True),

decodifier/cli.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
run_fixture_benchmark,
1616
)
1717
from .retrieval import get_context_read_plan, materialize_context, search_symbols
18+
from .tool_server import run_stdio_tool_server
1819

1920

2021
def _build_parser() -> argparse.ArgumentParser:
@@ -32,6 +33,11 @@ def _build_parser() -> argparse.ArgumentParser:
3233
action="store_true",
3334
help="Render the planned context after listing symbols",
3435
)
36+
query_parser.add_argument(
37+
"--debug",
38+
action="store_true",
39+
help="Print retrieval rationale and debug metadata for each symbol hit",
40+
)
3541

3642
benchmark_parser = subparsers.add_parser("benchmark", help="Run retrieval benchmarks against fixture repos")
3743
benchmark_parser.add_argument(
@@ -71,6 +77,12 @@ def _build_parser() -> argparse.ArgumentParser:
7177
default=str(DEFAULT_BENCHMARK_MARKDOWN_PATH),
7278
help="Path to write the markdown benchmark report",
7379
)
80+
81+
tool_server_parser = subparsers.add_parser(
82+
"tool-server",
83+
help="Run a stdio JSON tool server for local agent integration",
84+
)
85+
tool_server_parser.add_argument("--path", default=".", help="Repo root to serve tools against")
7486
return parser
7587

7688

@@ -84,6 +96,15 @@ def main(argv: Sequence[str] | None = None) -> int:
8496
for symbol in symbols:
8597
print(symbol["symbol"])
8698
print(f"{symbol['path']}:{symbol['start_line']}-{symbol['end_line']}")
99+
if args.debug:
100+
surfaces = symbol.get("behavior_surfaces") or []
101+
if surfaces:
102+
print(f" surfaces: {', '.join(surfaces)}")
103+
for reason in symbol.get("rationale") or []:
104+
print(f" why: {reason}")
105+
debug = symbol.get("debug") or {}
106+
if debug:
107+
print(f" debug: {json.dumps(debug, sort_keys=True)}")
87108

88109
if args.materialize:
89110
plan = get_context_read_plan(
@@ -120,6 +141,9 @@ def main(argv: Sequence[str] | None = None) -> int:
120141
print(markdown, end="")
121142
return 0
122143

144+
if args.command == "tool-server":
145+
return run_stdio_tool_server(Path(args.path).resolve())
146+
123147
parser.error(f"unknown command: {args.command}")
124148
return 2
125149

0 commit comments

Comments
 (0)