@pmaddire/gcie 0.1.6 → 0.1.8

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.
package/GCIE_USAGE.md CHANGED
@@ -288,3 +288,58 @@ query -> scope -> profile/budget escalation -> targeted gap-fill -> rg fallback.
288
288
  1. This file is intentionally generalized and adaptive for any repo.
289
289
  2. Keep repo-specific tuning in learned overrides and `.gcie` state, not in global defaults.
290
290
  3. If in doubt, choose the higher-accuracy path first, then optimize tokens after lock.
291
+
292
+ ## Cross-Repo Adaptation Rules (Required)
293
+
294
+ Use these rules to keep adaptation portable across repositories.
295
+
296
+ 1. Adaptation case source must be repo-local:
297
+ - Prefer generated cases from actual files in the target repo.
298
+ - Do not rely on hardcoded expected files from another codebase family.
299
+ - If report `case_source` is not repo-local, treat the run as invalid.
300
+
301
+ 2. Accuracy lock is required, but selection must be cost-aware:
302
+ - First gate: `100%` must-have full-hit.
303
+ - If multiple candidates pass `100%`, choose the lowest `tokens_per_expected_hit`.
304
+ - Do not keep `slices` as active default when a `plain` candidate also has `100%` and is cheaper.
305
+
306
+ 3. Near-miss rescue before expensive lock-in:
307
+ - If a cheaper candidate is below lock by one file/family (for example `90%`), run a short rescue cycle before accepting an expensive `100%` candidate:
308
+ 1) targeted gap-fill for missing must-have file(s)
309
+ 2) scope correction (subtree if clustered)
310
+ 3) one budget rung increase
311
+ - Re-evaluate after rescue; prefer the cheaper candidate if it reaches `100%`.
312
+
313
+ 4. Cost sanity guardrail:
314
+ - If selected active candidate is `>40%` more expensive than the cheapest candidate, mark status `accuracy_locked_but_cost_risky` and continue family-level refinement.
315
+ - Keep accuracy lock, but do not finalize global defaults until cost risk is reduced.
316
+
317
+ 5. Family-scoped finalization:
318
+ - Finalize routing per family, not as one global winner.
319
+ - Example: keep `slices` only for families where it is uniquely required for `100%`; use `plain` on families where it is cheaper at equal hit rate.
320
+
321
+ 6. Required report checks each run:
322
+ - `case_source`
323
+ - `full_hit_rate_pct`
324
+ - `tokens_per_query`
325
+ - `tokens_per_expected_hit`
326
+ - token delta between selected candidate and cheapest candidate
327
+
328
+ ## Portable Validation Checklist (Any New Repo)
329
+
330
+ After running adaptation:
331
+ 1. Confirm `status: ok`.
332
+ 2. Confirm `case_source: generated_repo_local`.
333
+ 3. Confirm `full_hit_rate_pct: 100` for selected final profile.
334
+ 4. Compare selected profile vs cheapest candidate:
335
+ - if selected is much more expensive, run one rescue iteration.
336
+ 5. Run a 50-query unique validation before trusting defaults broadly.
337
+
338
+ Commands:
339
+ ```powershell
340
+ gcie.cmd adapt . --benchmark-size 10 --efficiency-iterations 5 --clear-profile
341
+ ```
342
+ ```powershell
343
+ gcie.cmd adapt . --benchmark-size 10 --efficiency-iterations 5
344
+ ```
345
+
@@ -30,7 +30,28 @@ class CaseResult:
30
30
  context_complete: bool
31
31
 
32
32
 
33
+ @dataclass(frozen=True, slots=True)
34
+ class AdaptCase:
35
+ name: str
36
+ query: str
37
+ intent: str
38
+ baseline_files: tuple[str, ...]
39
+ expected_files: tuple[str, ...]
40
+
41
+
33
42
  _WORD_RE = re.compile(r"[A-Za-z0-9_./-]+")
43
+ _SOURCE_EXTS = {".py", ".js", ".jsx", ".ts", ".tsx", ".java", ".go", ".rs", ".cs", ".cpp", ".c", ".h"}
44
+ _IGNORED_DIRS = {
45
+ ".git",
46
+ ".gcie",
47
+ ".planning",
48
+ ".venv",
49
+ "node_modules",
50
+ "__pycache__",
51
+ "dist",
52
+ "build",
53
+ "coverage",
54
+ }
34
55
 
35
56
 
36
57
  def _query_keywords(text: str) -> list[str]:
@@ -252,7 +273,121 @@ def _summarize(label: str, rows: list[CaseResult]) -> dict:
252
273
  }
253
274
 
254
275
 
255
- def _write_back(repo_path: Path, best: dict) -> None:
276
+ def _collect_source_files(repo_path: Path) -> list[str]:
277
+ files: list[str] = []
278
+ for path in repo_path.rglob("*"):
279
+ if not path.is_file():
280
+ continue
281
+ rel = path.relative_to(repo_path)
282
+ if any(part in _IGNORED_DIRS for part in rel.parts):
283
+ continue
284
+ if path.suffix.lower() not in _SOURCE_EXTS:
285
+ continue
286
+ files.append(rel.as_posix())
287
+ return sorted(files)
288
+
289
+
290
+ def _static_cases_for_repo(repo_path: Path) -> list[AdaptCase]:
291
+ out: list[AdaptCase] = []
292
+ for case in list(BENCHMARK_CASES):
293
+ expected = tuple(case.expected_files)
294
+ if not expected:
295
+ continue
296
+ if not all((repo_path / rel).exists() for rel in expected):
297
+ continue
298
+ baseline = tuple(rel for rel in case.baseline_files if (repo_path / rel).exists())
299
+ if not baseline:
300
+ baseline = expected
301
+ out.append(
302
+ AdaptCase(
303
+ name=case.name,
304
+ query=case.query,
305
+ intent=case.intent,
306
+ baseline_files=baseline,
307
+ expected_files=expected,
308
+ )
309
+ )
310
+ return out
311
+
312
+
313
+ def _generated_cases_for_repo(repo_path: Path, needed: int) -> list[AdaptCase]:
314
+ files = _collect_source_files(repo_path)
315
+ if not files:
316
+ return []
317
+
318
+ by_dir: dict[str, list[str]] = {}
319
+ for rel in files:
320
+ parent = str(Path(rel).parent).replace("\\", "/")
321
+ by_dir.setdefault(parent, []).append(rel)
322
+
323
+ rows: list[AdaptCase] = []
324
+ seen_names: set[str] = set()
325
+
326
+ def add_case(name: str, expected: tuple[str, ...], intent: str = "explore") -> None:
327
+ if len(rows) >= needed:
328
+ return
329
+ safe_name = re.sub(r"[^a-zA-Z0-9_]+", "_", name).strip("_").lower() or "case"
330
+ if safe_name in seen_names:
331
+ idx = 2
332
+ while f"{safe_name}_{idx}" in seen_names:
333
+ idx += 1
334
+ safe_name = f"{safe_name}_{idx}"
335
+ seen_names.add(safe_name)
336
+ symbols = []
337
+ for rel in expected:
338
+ stem = Path(rel).stem.lower()
339
+ symbols.extend([stem, "flow", "wiring"])
340
+ query = f"{' '.join(expected)} {' '.join(symbols[:6])}".strip()
341
+ rows.append(
342
+ AdaptCase(
343
+ name=safe_name,
344
+ query=query,
345
+ intent=intent,
346
+ baseline_files=expected,
347
+ expected_files=expected,
348
+ )
349
+ )
350
+
351
+ # Single-file probes.
352
+ for rel in files:
353
+ add_case(f"single_{Path(rel).stem}", (rel,), intent="explore")
354
+ if len(rows) >= max(needed // 2, 1):
355
+ break
356
+
357
+ # Same-directory pairs.
358
+ for parent, group in sorted(by_dir.items(), key=lambda item: item[0]):
359
+ if len(group) < 2:
360
+ continue
361
+ group = sorted(group)
362
+ for idx in range(len(group) - 1):
363
+ add_case(f"pair_{parent}_{idx}", (group[idx], group[idx + 1]), intent="explore")
364
+ if len(rows) >= needed:
365
+ return rows[:needed]
366
+
367
+ # Cross-directory pairs if still needed.
368
+ tops: dict[str, str] = {}
369
+ for rel in files:
370
+ top = Path(rel).parts[0] if Path(rel).parts else rel
371
+ tops.setdefault(top, rel)
372
+ top_files = list(tops.values())
373
+ for idx in range(len(top_files) - 1):
374
+ add_case(f"cross_{idx}", (top_files[idx], top_files[idx + 1]), intent="explore")
375
+ if len(rows) >= needed:
376
+ break
377
+
378
+ return rows[:needed]
379
+
380
+
381
+ def _select_adaptation_cases(repo_path: Path, benchmark_size: int) -> tuple[list[AdaptCase], str]:
382
+ """Select adaptation cases generated entirely from the target repo."""
383
+ benchmark_size = max(1, int(benchmark_size))
384
+ generated = _generated_cases_for_repo(repo_path, benchmark_size)
385
+ if generated:
386
+ return generated[:benchmark_size], "generated_repo_local"
387
+ return [], "none_available"
388
+
389
+
390
+ def _write_back(repo_path: Path, best: dict, case_source: str, pipeline_status: str, cost_analysis: dict) -> None:
256
391
  cfg_path = repo_path / ".gcie" / "context_config.json"
257
392
  if cfg_path.exists():
258
393
  try:
@@ -264,10 +399,12 @@ def _write_back(repo_path: Path, best: dict) -> None:
264
399
  else:
265
400
  cfg = {}
266
401
  cfg["adaptation_pipeline"] = {
267
- "status": "complete",
402
+ "status": pipeline_status,
268
403
  "best_label": best.get("label"),
269
404
  "full_hit_rate_pct": best.get("full_hit_rate_pct"),
270
405
  "tokens_per_query": best.get("tokens_per_query"),
406
+ "case_source": case_source,
407
+ "cost_analysis": cost_analysis,
271
408
  "updated_at": datetime.now(timezone.utc).isoformat(),
272
409
  }
273
410
  cfg_path.parent.mkdir(parents=True, exist_ok=True)
@@ -290,17 +427,15 @@ def run_post_init_adaptation(
290
427
 
291
428
  clear_adaptive_profile(repo_path.as_posix())
292
429
 
293
- cases = list(BENCHMARK_CASES)
430
+ cases, case_source = _select_adaptation_cases(repo_path, benchmark_size)
294
431
  if not cases:
295
432
  return {
296
433
  "status": "no_benchmark_cases",
297
434
  "repo": repo_path.as_posix(),
298
- "message": "No benchmark cases available for accuracy-locked adaptation.",
435
+ "case_source": case_source,
436
+ "message": "No repo-usable adaptation cases available.",
299
437
  }
300
438
 
301
- benchmark_size = max(1, min(len(cases), int(benchmark_size)))
302
- cases = cases[:benchmark_size]
303
-
304
439
  slices_rows = [_evaluate_slices_case(case) for case in cases]
305
440
  plain_rows = [_evaluate_plain_case(case, allow_gapfill=False) for case in cases]
306
441
  plain_gap_rows = [_evaluate_plain_case(case, allow_gapfill=True) for case in cases]
@@ -327,13 +462,37 @@ def run_post_init_adaptation(
327
462
  if trial["full_hit_rate_pct"] >= active["full_hit_rate_pct"] and trial["tokens_per_query"] < active["tokens_per_query"]:
328
463
  active = trial
329
464
 
330
- _write_back(repo_path, active)
465
+ cheapest = min(candidates, key=lambda item: (item["tokens_per_expected_hit"] or 10**9, item["tokens_per_query"]))
466
+ token_delta = int(active["total_tokens"] - cheapest["total_tokens"])
467
+ pct_delta = round((token_delta / max(1, int(cheapest["total_tokens"]))) * 100, 1)
468
+
469
+ pipeline_status = "ok"
470
+ if (
471
+ active.get("full_hit_rate_pct", 0.0) >= 100.0
472
+ and active.get("label") != cheapest.get("label")
473
+ and pct_delta > 40.0
474
+ ):
475
+ pipeline_status = "accuracy_locked_but_cost_risky"
476
+
477
+ cost_analysis = {
478
+ "cheapest_label": cheapest.get("label"),
479
+ "selected_label": active.get("label"),
480
+ "selected_vs_cheapest_token_delta": token_delta,
481
+ "selected_vs_cheapest_pct_delta": pct_delta,
482
+ "risk_threshold_pct": 40.0,
483
+ "cost_risky": pipeline_status == "accuracy_locked_but_cost_risky",
484
+ }
485
+
486
+ _write_back(repo_path, active, case_source, pipeline_status, cost_analysis)
331
487
 
332
488
  report = {
333
- "status": "ok",
489
+ "status": pipeline_status,
334
490
  "repo": repo_path.as_posix(),
335
- "benchmark_size": benchmark_size,
491
+ "benchmark_size": len(cases),
492
+ "requested_benchmark_size": int(benchmark_size),
336
493
  "efficiency_iterations": int(efficiency_iterations),
494
+ "case_source": case_source,
495
+ "cost_analysis": cost_analysis,
337
496
  "stages": {
338
497
  "accuracy_candidates": [slices_summary, plain_summary, plain_gap_summary],
339
498
  "selected_after_accuracy": best,
@@ -348,5 +507,3 @@ def run_post_init_adaptation(
348
507
  out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
349
508
  report["report_path"] = out_path.as_posix()
350
509
  return report
351
-
352
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmaddire/gcie",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "GraphCode Intelligence Engine one-command setup and context CLI",
5
5
  "bin": {
6
6
  "gcie": "bin/gcie.js",