@pmaddire/gcie 0.1.5 → 0.1.6

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.
@@ -1,341 +1,352 @@
1
- """Post-initialization adaptation pipeline (accuracy first, then efficiency)."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import asdict, dataclass
6
- from datetime import datetime, timezone
7
- import json
8
- import re
9
- from pathlib import Path
10
-
11
- from .context import run_context
12
- from .context_slices import _classify_query_family, run_context_slices
13
- from .index import run_index
14
-
15
- try:
16
- from performance.context_benchmark import BENCHMARK_CASES
17
- except Exception: # pragma: no cover - fallback for limited installs
18
- BENCHMARK_CASES = ()
19
-
20
-
21
- @dataclass(frozen=True, slots=True)
22
- class CaseResult:
23
- name: str
24
- family: str
25
- mode: str
26
- tokens: int
27
- expected_hits: int
28
- expected_total: int
29
- missing_expected: tuple[str, ...]
30
- context_complete: bool
31
-
32
-
33
- _WORD_RE = re.compile(r"[A-Za-z0-9_./-]+")
34
-
35
-
36
- def _query_keywords(text: str) -> list[str]:
37
- terms: list[str] = []
38
- for token in _WORD_RE.findall(text.lower()):
39
- if len(token) < 4:
40
- continue
41
- terms.append(token)
42
- return terms[:8]
43
-
44
-
45
- def _node_to_file(node_id: str) -> str | None:
46
- if node_id.startswith("file:"):
47
- return node_id[5:]
48
- if node_id.startswith("function:"):
49
- return node_id[9:].split("::", 1)[0]
50
- if node_id.startswith("class:"):
51
- return node_id[6:].split("::", 1)[0]
52
- return None
53
-
54
-
55
- def _normalize_scoped_path(plan_path: str, rel_path: str) -> str:
56
- normalized = rel_path.replace("\\", "/").lstrip("./")
57
- if not plan_path or plan_path in {".", "./"}:
58
- return normalized
59
- base = Path(plan_path).as_posix().strip("/")
60
- if normalized.startswith(base + "/") or normalized == base:
61
- return normalized
62
- return f"{base}/{normalized}"
63
-
64
-
65
- def _family_path(expected_files: tuple[str, ...]) -> str:
66
- if not expected_files:
67
- return "."
68
- heads = {Path(p).parts[0] for p in expected_files if Path(p).parts}
69
- if len(heads) == 1:
70
- return next(iter(heads))
71
- return "."
72
-
73
-
74
- def _plan_query(case) -> tuple[str, str, int | None]:
75
- path = _family_path(case.expected_files)
76
- if getattr(case, "name", "") == "cli_context_command":
77
- path = "."
78
- query = "cli/commands/context.py llm_context/context_builder.py build_context token_budget mandatory_node_ids snippet_selector"
79
- return path, query, 950
80
- keywords = " ".join(_query_keywords(case.query)[:4])
81
- file_terms = " ".join(case.expected_files)
82
- query = f"{file_terms} {keywords}".strip()
83
- budget = 1000 if len(case.expected_files) >= 2 else None
84
- if getattr(case, "name", "") in {
85
- "repository_scanner_filters",
86
- "knowledge_index_query_api",
87
- "execution_trace_graph",
88
- "parser_fallbacks",
89
- }:
90
- budget = 800
91
- return path, query, budget
92
-
93
-
94
- def _evaluate_plain_case(case, *, allow_gapfill: bool = True) -> CaseResult:
95
- path, query, budget = _plan_query(case)
96
- payload = run_context(path, query, budget=budget, intent=case.intent)
97
- files = {
98
- _normalize_scoped_path(path, rel_path)
99
- for rel_path in (_node_to_file(item.get("node_id", "")) for item in payload.get("snippets", []))
100
- if rel_path
101
- }
102
- expected = tuple(case.expected_files)
103
- missing = [rel for rel in expected if rel not in files]
104
- tokens = int(payload.get("tokens", 0) or 0)
105
- mode = "plain_context_workflow"
106
-
107
- if allow_gapfill and missing:
108
- mode = "plain_context_workflow_gapfill"
109
- for rel in list(missing):
110
- scope = _family_path((rel,))
111
- gap_keywords = " ".join(_query_keywords(case.query)[:4])
112
- gap_query = f"{rel} {gap_keywords}".strip()
113
- gap_budget = 500 if rel.endswith("/main.py") or rel == "main.py" else 900
114
- gap_payload = run_context(scope, gap_query, budget=gap_budget, intent=case.intent)
115
- tokens += int(gap_payload.get("tokens", 0) or 0)
116
- gap_files = {
117
- _normalize_scoped_path(scope, rel_path)
118
- for rel_path in (_node_to_file(item.get("node_id", "")) for item in gap_payload.get("snippets", []))
119
- if rel_path
120
- }
121
- files.update(gap_files)
122
- missing = [m for m in expected if m not in files]
123
- if not missing:
124
- break
125
-
126
- expected_hits = len(expected) - len(missing)
127
- family = _classify_query_family(query)
128
- return CaseResult(
129
- name=case.name,
130
- family=family,
131
- mode=mode,
132
- tokens=tokens,
133
- expected_hits=expected_hits,
134
- expected_total=len(expected),
135
- missing_expected=tuple(missing),
136
- context_complete=not missing,
137
- )
138
-
139
-
140
- def _evaluate_slices_case(case) -> CaseResult:
141
- payload = run_context_slices(
142
- repo=".",
143
- query=case.query,
144
- profile="low",
145
- stage_a_budget=300,
146
- stage_b_budget=600,
147
- max_total=800,
148
- intent=case.intent,
149
- pin=None,
150
- pin_budget=200,
151
- include_tests=False,
152
- )
153
- mode = "slices_low"
154
- tokens = int(payload.get("token_estimate", payload.get("tokens", 0)) or 0)
155
- files = {
156
- _node_to_file(item.get("node_id", ""))
157
- for item in payload.get("snippets", [])
158
- }
159
- files = {f for f in files if f}
160
- expected = tuple(case.expected_files)
161
- missing = [rel for rel in expected if rel not in files]
162
- if missing:
163
- mode = "slices_recall"
164
- recall_payload = run_context_slices(
165
- repo=".",
166
- query=case.query,
167
- profile="recall",
168
- stage_a_budget=400,
169
- stage_b_budget=800,
170
- max_total=1200,
171
- intent=case.intent,
172
- pin=None,
173
- pin_budget=300,
174
- include_tests=False,
175
- )
176
- tokens += int(recall_payload.get("token_estimate", recall_payload.get("tokens", 0)) or 0)
177
- files.update(
178
- {
179
- f
180
- for f in (_node_to_file(item.get("node_id", "")) for item in recall_payload.get("snippets", []))
181
- if f
182
- }
183
- )
184
- missing = [rel for rel in expected if rel not in files]
185
- if missing:
186
- mode = "slices_recall_pin"
187
- for rel in list(missing):
188
- pin_payload = run_context_slices(
189
- repo=".",
190
- query=case.query,
191
- profile="recall",
192
- stage_a_budget=400,
193
- stage_b_budget=800,
194
- max_total=1200,
195
- intent=case.intent,
196
- pin=rel,
197
- pin_budget=300,
198
- include_tests=False,
199
- )
200
- tokens += int(pin_payload.get("token_estimate", pin_payload.get("tokens", 0)) or 0)
201
- files.update(
202
- {
203
- f
204
- for f in (_node_to_file(item.get("node_id", "")) for item in pin_payload.get("snippets", []))
205
- if f
206
- }
207
- )
208
- missing = [m for m in expected if m not in files]
209
- if not missing:
210
- break
211
- expected_hits = len(expected) - len(missing)
212
- family = _classify_query_family(case.query)
213
- return CaseResult(
214
- name=case.name,
215
- family=family,
216
- mode=mode,
217
- tokens=tokens,
218
- expected_hits=expected_hits,
219
- expected_total=len(expected),
220
- missing_expected=tuple(missing),
221
- context_complete=not missing,
222
- )
223
-
224
-
225
- def _summarize(label: str, rows: list[CaseResult]) -> dict:
226
- case_count = len(rows)
227
- pass_count = sum(1 for row in rows if row.context_complete)
228
- total_tokens = sum(row.tokens for row in rows)
229
- hit_count = sum(row.expected_hits for row in rows)
230
- hit_total = sum(row.expected_total for row in rows)
231
- return {
232
- "label": label,
233
- "case_count": case_count,
234
- "passing_cases": pass_count,
235
- "full_hit_rate_pct": round((pass_count / case_count) * 100, 1) if case_count else 0.0,
236
- "target_hit_rate_pct": round((hit_count / hit_total) * 100, 1) if hit_total else 0.0,
237
- "total_tokens": total_tokens,
238
- "tokens_per_query": round(total_tokens / case_count, 1) if case_count else 0.0,
239
- "tokens_per_expected_hit": round(total_tokens / hit_count, 2) if hit_count else None,
240
- "results": [asdict(row) for row in rows],
241
- }
242
-
243
-
244
- def _write_back(repo_path: Path, best: dict) -> None:
245
- cfg_path = repo_path / ".gcie" / "context_config.json"
246
- if cfg_path.exists():
247
- try:
248
- cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
249
- if not isinstance(cfg, dict):
250
- cfg = {}
251
- except Exception:
252
- cfg = {}
253
- else:
254
- cfg = {}
255
- cfg["adaptation_pipeline"] = {
256
- "status": "complete",
257
- "best_label": best.get("label"),
258
- "full_hit_rate_pct": best.get("full_hit_rate_pct"),
259
- "tokens_per_query": best.get("tokens_per_query"),
260
- "updated_at": datetime.now(timezone.utc).isoformat(),
261
- }
262
- cfg_path.parent.mkdir(parents=True, exist_ok=True)
263
- cfg_path.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
264
-
265
-
266
- def run_post_init_adaptation(
267
- repo: str = ".",
268
- *,
269
- benchmark_size: int = 10,
270
- efficiency_iterations: int = 5,
271
- clear_profile: bool = False,
272
- ) -> dict:
273
- """Run accuracy-lock then efficiency adaptation protocol after setup/index."""
274
- repo_path = Path(repo).resolve()
275
- run_index(repo_path.as_posix())
276
-
277
- if clear_profile:
278
- from .context_slices import clear_adaptive_profile
279
-
280
- clear_adaptive_profile(repo_path.as_posix())
281
-
282
- cases = list(BENCHMARK_CASES)
283
- if not cases:
284
- return {
285
- "status": "no_benchmark_cases",
286
- "repo": repo_path.as_posix(),
287
- "message": "No benchmark cases available for accuracy-locked adaptation.",
288
- }
289
-
290
- benchmark_size = max(1, min(len(cases), int(benchmark_size)))
291
- cases = cases[:benchmark_size]
292
-
293
- slices_rows = [_evaluate_slices_case(case) for case in cases]
294
- plain_rows = [_evaluate_plain_case(case, allow_gapfill=False) for case in cases]
295
- plain_gap_rows = [_evaluate_plain_case(case, allow_gapfill=True) for case in cases]
296
-
297
- slices_summary = _summarize("slices_accuracy_stage", slices_rows)
298
- plain_summary = _summarize("plain_accuracy_stage", plain_rows)
299
- plain_gap_summary = _summarize("plain_gapfill_accuracy_stage", plain_gap_rows)
300
-
301
- candidates = [slices_summary, plain_summary, plain_gap_summary]
302
- full_hit = [candidate for candidate in candidates if candidate["full_hit_rate_pct"] >= 100.0]
303
- if full_hit:
304
- best = min(full_hit, key=lambda item: (item["tokens_per_expected_hit"] or 10**9, item["tokens_per_query"]))
305
- else:
306
- best = max(candidates, key=lambda item: item["target_hit_rate_pct"])
307
-
308
- efficiency_trials: list[dict] = []
309
- active = best
310
- for idx in range(max(0, int(efficiency_iterations))):
311
- if active["label"] != "plain_gapfill_accuracy_stage":
312
- break
313
- trial_rows = [_evaluate_plain_case(case, allow_gapfill=True) for case in cases]
314
- trial = _summarize(f"plain_gapfill_eff_trial_{idx + 1}", trial_rows)
315
- efficiency_trials.append(trial)
316
- if trial["full_hit_rate_pct"] >= active["full_hit_rate_pct"] and trial["tokens_per_query"] < active["tokens_per_query"]:
317
- active = trial
318
-
319
- _write_back(repo_path, active)
320
-
321
- report = {
322
- "status": "ok",
323
- "repo": repo_path.as_posix(),
324
- "benchmark_size": benchmark_size,
325
- "efficiency_iterations": int(efficiency_iterations),
326
- "stages": {
327
- "accuracy_candidates": [slices_summary, plain_summary, plain_gap_summary],
328
- "selected_after_accuracy": best,
329
- "efficiency_trials": efficiency_trials,
330
- "selected_final": active,
331
- },
332
- }
333
-
334
- planning_dir = repo_path / ".planning"
335
- planning_dir.mkdir(parents=True, exist_ok=True)
336
- out_path = planning_dir / "post_init_adaptation_report.json"
337
- out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
338
- report["report_path"] = out_path.as_posix()
1
+ """Post-initialization adaptation pipeline (accuracy first, then efficiency)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from datetime import datetime, timezone
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+
11
+ from .context import run_context
12
+ from .context_slices import _classify_query_family, run_context_slices
13
+ from .index import run_index
14
+
15
+ try:
16
+ from performance.context_benchmark import BENCHMARK_CASES
17
+ except Exception: # pragma: no cover - fallback for limited installs
18
+ BENCHMARK_CASES = ()
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class CaseResult:
23
+ name: str
24
+ family: str
25
+ mode: str
26
+ tokens: int
27
+ expected_hits: int
28
+ expected_total: int
29
+ missing_expected: tuple[str, ...]
30
+ context_complete: bool
31
+
32
+
33
+ _WORD_RE = re.compile(r"[A-Za-z0-9_./-]+")
34
+
35
+
36
+ def _query_keywords(text: str) -> list[str]:
37
+ terms: list[str] = []
38
+ for token in _WORD_RE.findall(text.lower()):
39
+ if len(token) < 4:
40
+ continue
41
+ terms.append(token)
42
+ return terms[:8]
43
+
44
+
45
+ def _node_to_file(node_id: str) -> str | None:
46
+ if node_id.startswith("file:"):
47
+ return node_id[5:]
48
+ if node_id.startswith("function:"):
49
+ return node_id[9:].split("::", 1)[0]
50
+ if node_id.startswith("class:"):
51
+ return node_id[6:].split("::", 1)[0]
52
+ return None
53
+
54
+
55
+ def _normalize_scoped_path(plan_path: str, rel_path: str) -> str:
56
+ normalized = rel_path.replace("\\", "/").lstrip("./")
57
+ if not plan_path or plan_path in {".", "./"}:
58
+ return normalized
59
+ base = Path(plan_path).as_posix().strip("/")
60
+ if normalized.startswith(base + "/") or normalized == base:
61
+ return normalized
62
+ return f"{base}/{normalized}"
63
+
64
+
65
+ def _family_path(expected_files: tuple[str, ...]) -> str:
66
+ if not expected_files:
67
+ return "."
68
+ heads = {Path(p).parts[0] for p in expected_files if Path(p).parts}
69
+ if len(heads) == 1:
70
+ return next(iter(heads))
71
+ return "."
72
+
73
+
74
+ def _safe_scope(path: str) -> str:
75
+ """Return a valid retrieval scope for the current repo."""
76
+ if not path or path in {".", "./"}:
77
+ return "."
78
+ candidate = Path(path)
79
+ if candidate.exists() and candidate.is_dir():
80
+ return candidate.as_posix()
81
+ return "."
82
+
83
+
84
+ def _plan_query(case) -> tuple[str, str, int | None]:
85
+ path = _family_path(case.expected_files)
86
+ if getattr(case, "name", "") == "cli_context_command":
87
+ path = "."
88
+ query = "cli/commands/context.py llm_context/context_builder.py build_context token_budget mandatory_node_ids snippet_selector"
89
+ return path, query, 950
90
+ keywords = " ".join(_query_keywords(case.query)[:4])
91
+ file_terms = " ".join(case.expected_files)
92
+ query = f"{file_terms} {keywords}".strip()
93
+ budget = 1000 if len(case.expected_files) >= 2 else None
94
+ if getattr(case, "name", "") in {
95
+ "repository_scanner_filters",
96
+ "knowledge_index_query_api",
97
+ "execution_trace_graph",
98
+ "parser_fallbacks",
99
+ }:
100
+ budget = 800
101
+ return path, query, budget
102
+
103
+
104
+ def _evaluate_plain_case(case, *, allow_gapfill: bool = True) -> CaseResult:
105
+ path, query, budget = _plan_query(case)
106
+ path = _safe_scope(path)
107
+ payload = run_context(path, query, budget=budget, intent=case.intent)
108
+ files = {
109
+ _normalize_scoped_path(path, rel_path)
110
+ for rel_path in (_node_to_file(item.get("node_id", "")) for item in payload.get("snippets", []))
111
+ if rel_path
112
+ }
113
+ expected = tuple(case.expected_files)
114
+ missing = [rel for rel in expected if rel not in files]
115
+ tokens = int(payload.get("tokens", 0) or 0)
116
+ mode = "plain_context_workflow"
117
+
118
+ if allow_gapfill and missing:
119
+ mode = "plain_context_workflow_gapfill"
120
+ for rel in list(missing):
121
+ scope = _safe_scope(_family_path((rel,)))
122
+ gap_keywords = " ".join(_query_keywords(case.query)[:4])
123
+ gap_query = f"{rel} {gap_keywords}".strip()
124
+ gap_budget = 500 if rel.endswith("/main.py") or rel == "main.py" else 900
125
+ gap_payload = run_context(scope, gap_query, budget=gap_budget, intent=case.intent)
126
+ tokens += int(gap_payload.get("tokens", 0) or 0)
127
+ gap_files = {
128
+ _normalize_scoped_path(scope, rel_path)
129
+ for rel_path in (_node_to_file(item.get("node_id", "")) for item in gap_payload.get("snippets", []))
130
+ if rel_path
131
+ }
132
+ files.update(gap_files)
133
+ missing = [m for m in expected if m not in files]
134
+ if not missing:
135
+ break
136
+
137
+ expected_hits = len(expected) - len(missing)
138
+ family = _classify_query_family(query)
139
+ return CaseResult(
140
+ name=case.name,
141
+ family=family,
142
+ mode=mode,
143
+ tokens=tokens,
144
+ expected_hits=expected_hits,
145
+ expected_total=len(expected),
146
+ missing_expected=tuple(missing),
147
+ context_complete=not missing,
148
+ )
149
+
150
+
151
+ def _evaluate_slices_case(case) -> CaseResult:
152
+ payload = run_context_slices(
153
+ repo=".",
154
+ query=case.query,
155
+ profile="low",
156
+ stage_a_budget=300,
157
+ stage_b_budget=600,
158
+ max_total=800,
159
+ intent=case.intent,
160
+ pin=None,
161
+ pin_budget=200,
162
+ include_tests=False,
163
+ )
164
+ mode = "slices_low"
165
+ tokens = int(payload.get("token_estimate", payload.get("tokens", 0)) or 0)
166
+ files = {
167
+ _node_to_file(item.get("node_id", ""))
168
+ for item in payload.get("snippets", [])
169
+ }
170
+ files = {f for f in files if f}
171
+ expected = tuple(case.expected_files)
172
+ missing = [rel for rel in expected if rel not in files]
173
+ if missing:
174
+ mode = "slices_recall"
175
+ recall_payload = run_context_slices(
176
+ repo=".",
177
+ query=case.query,
178
+ profile="recall",
179
+ stage_a_budget=400,
180
+ stage_b_budget=800,
181
+ max_total=1200,
182
+ intent=case.intent,
183
+ pin=None,
184
+ pin_budget=300,
185
+ include_tests=False,
186
+ )
187
+ tokens += int(recall_payload.get("token_estimate", recall_payload.get("tokens", 0)) or 0)
188
+ files.update(
189
+ {
190
+ f
191
+ for f in (_node_to_file(item.get("node_id", "")) for item in recall_payload.get("snippets", []))
192
+ if f
193
+ }
194
+ )
195
+ missing = [rel for rel in expected if rel not in files]
196
+ if missing:
197
+ mode = "slices_recall_pin"
198
+ for rel in list(missing):
199
+ pin_payload = run_context_slices(
200
+ repo=".",
201
+ query=case.query,
202
+ profile="recall",
203
+ stage_a_budget=400,
204
+ stage_b_budget=800,
205
+ max_total=1200,
206
+ intent=case.intent,
207
+ pin=rel,
208
+ pin_budget=300,
209
+ include_tests=False,
210
+ )
211
+ tokens += int(pin_payload.get("token_estimate", pin_payload.get("tokens", 0)) or 0)
212
+ files.update(
213
+ {
214
+ f
215
+ for f in (_node_to_file(item.get("node_id", "")) for item in pin_payload.get("snippets", []))
216
+ if f
217
+ }
218
+ )
219
+ missing = [m for m in expected if m not in files]
220
+ if not missing:
221
+ break
222
+ expected_hits = len(expected) - len(missing)
223
+ family = _classify_query_family(case.query)
224
+ return CaseResult(
225
+ name=case.name,
226
+ family=family,
227
+ mode=mode,
228
+ tokens=tokens,
229
+ expected_hits=expected_hits,
230
+ expected_total=len(expected),
231
+ missing_expected=tuple(missing),
232
+ context_complete=not missing,
233
+ )
234
+
235
+
236
+ def _summarize(label: str, rows: list[CaseResult]) -> dict:
237
+ case_count = len(rows)
238
+ pass_count = sum(1 for row in rows if row.context_complete)
239
+ total_tokens = sum(row.tokens for row in rows)
240
+ hit_count = sum(row.expected_hits for row in rows)
241
+ hit_total = sum(row.expected_total for row in rows)
242
+ return {
243
+ "label": label,
244
+ "case_count": case_count,
245
+ "passing_cases": pass_count,
246
+ "full_hit_rate_pct": round((pass_count / case_count) * 100, 1) if case_count else 0.0,
247
+ "target_hit_rate_pct": round((hit_count / hit_total) * 100, 1) if hit_total else 0.0,
248
+ "total_tokens": total_tokens,
249
+ "tokens_per_query": round(total_tokens / case_count, 1) if case_count else 0.0,
250
+ "tokens_per_expected_hit": round(total_tokens / hit_count, 2) if hit_count else None,
251
+ "results": [asdict(row) for row in rows],
252
+ }
253
+
254
+
255
+ def _write_back(repo_path: Path, best: dict) -> None:
256
+ cfg_path = repo_path / ".gcie" / "context_config.json"
257
+ if cfg_path.exists():
258
+ try:
259
+ cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
260
+ if not isinstance(cfg, dict):
261
+ cfg = {}
262
+ except Exception:
263
+ cfg = {}
264
+ else:
265
+ cfg = {}
266
+ cfg["adaptation_pipeline"] = {
267
+ "status": "complete",
268
+ "best_label": best.get("label"),
269
+ "full_hit_rate_pct": best.get("full_hit_rate_pct"),
270
+ "tokens_per_query": best.get("tokens_per_query"),
271
+ "updated_at": datetime.now(timezone.utc).isoformat(),
272
+ }
273
+ cfg_path.parent.mkdir(parents=True, exist_ok=True)
274
+ cfg_path.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
275
+
276
+
277
+ def run_post_init_adaptation(
278
+ repo: str = ".",
279
+ *,
280
+ benchmark_size: int = 10,
281
+ efficiency_iterations: int = 5,
282
+ clear_profile: bool = False,
283
+ ) -> dict:
284
+ """Run accuracy-lock then efficiency adaptation protocol after setup/index."""
285
+ repo_path = Path(repo).resolve()
286
+ run_index(repo_path.as_posix())
287
+
288
+ if clear_profile:
289
+ from .context_slices import clear_adaptive_profile
290
+
291
+ clear_adaptive_profile(repo_path.as_posix())
292
+
293
+ cases = list(BENCHMARK_CASES)
294
+ if not cases:
295
+ return {
296
+ "status": "no_benchmark_cases",
297
+ "repo": repo_path.as_posix(),
298
+ "message": "No benchmark cases available for accuracy-locked adaptation.",
299
+ }
300
+
301
+ benchmark_size = max(1, min(len(cases), int(benchmark_size)))
302
+ cases = cases[:benchmark_size]
303
+
304
+ slices_rows = [_evaluate_slices_case(case) for case in cases]
305
+ plain_rows = [_evaluate_plain_case(case, allow_gapfill=False) for case in cases]
306
+ plain_gap_rows = [_evaluate_plain_case(case, allow_gapfill=True) for case in cases]
307
+
308
+ slices_summary = _summarize("slices_accuracy_stage", slices_rows)
309
+ plain_summary = _summarize("plain_accuracy_stage", plain_rows)
310
+ plain_gap_summary = _summarize("plain_gapfill_accuracy_stage", plain_gap_rows)
311
+
312
+ candidates = [slices_summary, plain_summary, plain_gap_summary]
313
+ full_hit = [candidate for candidate in candidates if candidate["full_hit_rate_pct"] >= 100.0]
314
+ if full_hit:
315
+ best = min(full_hit, key=lambda item: (item["tokens_per_expected_hit"] or 10**9, item["tokens_per_query"]))
316
+ else:
317
+ best = max(candidates, key=lambda item: item["target_hit_rate_pct"])
318
+
319
+ efficiency_trials: list[dict] = []
320
+ active = best
321
+ for idx in range(max(0, int(efficiency_iterations))):
322
+ if active["label"] != "plain_gapfill_accuracy_stage":
323
+ break
324
+ trial_rows = [_evaluate_plain_case(case, allow_gapfill=True) for case in cases]
325
+ trial = _summarize(f"plain_gapfill_eff_trial_{idx + 1}", trial_rows)
326
+ efficiency_trials.append(trial)
327
+ if trial["full_hit_rate_pct"] >= active["full_hit_rate_pct"] and trial["tokens_per_query"] < active["tokens_per_query"]:
328
+ active = trial
329
+
330
+ _write_back(repo_path, active)
331
+
332
+ report = {
333
+ "status": "ok",
334
+ "repo": repo_path.as_posix(),
335
+ "benchmark_size": benchmark_size,
336
+ "efficiency_iterations": int(efficiency_iterations),
337
+ "stages": {
338
+ "accuracy_candidates": [slices_summary, plain_summary, plain_gap_summary],
339
+ "selected_after_accuracy": best,
340
+ "efficiency_trials": efficiency_trials,
341
+ "selected_final": active,
342
+ },
343
+ }
344
+
345
+ planning_dir = repo_path / ".planning"
346
+ planning_dir.mkdir(parents=True, exist_ok=True)
347
+ out_path = planning_dir / "post_init_adaptation_report.json"
348
+ out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
349
+ report["report_path"] = out_path.as_posix()
339
350
  return report
340
351
 
341
352
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmaddire/gcie",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "GraphCode Intelligence Engine one-command setup and context CLI",
5
5
  "bin": {
6
6
  "gcie": "bin/gcie.js",