codebase-recall 0.7.3

CLI based application for codebase dumper for LLMs and fast review/recall project
<#
.SYNOPSIS
  Guided, re-runnable walkthrough of the layered reference resolver (PowerShell).
.DESCRIPTION
  Runs `code-rcl graph` on the two demo projects and tags every `calls` edge with
  the layer that produced it (L1 scope / L2 import binding / L3 receiver type /
  L4 disambiguation), read off the confidence plus the source ref.

  Touches only docs/demo/*/.code-rcl (created then removed). With -Before it also
  builds `main` for contrast, using a `git stash` that is always restored.
.EXAMPLE
  powershell -ExecutionPolicy Bypass -File docs\demo\walkthrough.ps1
.EXAMPLE
  powershell -ExecutionPolicy Bypass -File docs\demo\walkthrough.ps1 -Before
#>
[CmdletBinding()]
param([switch]$Before)

# Native tools (cargo / git / code-rcl) write progress to stderr; under 'Stop'
# PowerShell 5.1 turns that into a terminating error. Use 'Continue' and check
# $LASTEXITCODE explicitly instead.
$ErrorActionPreference = 'Continue'

$root = (& git rev-parse --show-toplevel).Trim()
Set-Location $root

$bin = Join-Path $root 'target\debug\code-rcl.exe'

# --- find a real Python (skip the Windows Store "python3" alias) --------------
$py = $null
foreach ($c in 'python', 'py', 'python3') {
    if (-not (Get-Command $c -ErrorAction SilentlyContinue)) { continue }
    try {
        & $c -c 'import sys' 2>$null
        if ($LASTEXITCODE -eq 0) { $py = $c; break }
    } catch {}
}
if (-not $py) { Write-Error 'python not found (need python / py / python3)'; exit 1 }

function Build([string]$what) {
    Write-Host "building $what ..."
    & cargo build -q
    if ($LASTEXITCODE -ne 0) { Write-Error 'build failed'; exit 1 }
}

function Say([string]$text) {
    Write-Host ''
    Write-Host $text -ForegroundColor White
}

# --- shared analyzer: parse the graph JSON + cache.db, print tagged edges ----
$analyzer = @'
import json, sqlite3, sys, os
gj, dbp, label = sys.argv[1], sys.argv[2], sys.argv[3]
raw = len(sys.argv) > 4 and sys.argv[4] == "raw"
g = json.load(open(gj))
nid = {n["id"]: n for n in g["nodes"]}

if raw:
    print()
    print(f"### {label}")
    bar = "-" * 78
    print(bar)
    calls = sorted((e for e in g["edges"] if e["kind"] == "calls"),
                   key=lambda e: (nid[e["source"]].get("path", ""), nid[e["source"]]["label"]))
    if not calls:
        print("  (no call edges)")
    for e in calls:
        s, t = nid[e["source"]], nid[e["target"]]
        tline = (t.get("lines") or [None])[0]
        tloc = f"@{tline}" if tline else ""
        print(f"  {s.get('path','?')}::{s['label']:<12} ->  {t.get('path','?')}::{t['label']}{tloc}"
              f"   (conf {e['confidence']:.2f})")
    print(bar)
    sys.exit(0)

sym_by_id, field_types, all_names = {}, {}, set()
refs = []
if os.path.exists(dbp):
    c = sqlite3.connect(dbp)
    for sid, path, name, kind, tn, sl in c.execute(
        "select s.id,f.path,s.name,s.kind,s.type_name,s.start_line "
        "from symbols s join files f on f.id=s.file_id"):
        sym_by_id[sid] = dict(path=path, name=name, kind=kind, type_name=tn, line=sl)
        all_names.add(name)
    for owner_name, fname, ftype in c.execute(
        "select os.name, b.name, b.type_expr "
        "from bindings b join scopes sc on sc.id=b.scope_id "
        "join symbols os on os.id=sc.owner_symbol_id "
        "where b.binding_kind='field' and b.type_expr is not null"):
        field_types[(owner_name, fname)] = ftype
    for path, esid, rn, recv, rk, loc, resolved, rkind in c.execute(
        "select f.path,r.from_symbol_id,r.name,r.receiver,r.receiver_kind,"
        "r.local_only,r.resolved_symbol_id,r.ref_kind "
        "from refs r join files f on f.id=r.file_id"):
        refs.append(dict(path=path, esid=esid, name=rn, recv=recv, rk=rk,
                         loc=loc, resolved=resolved, kind=rkind))

def chain_type(enclosing_type, recv):
    segs = (recv or "").split(".")
    cur = enclosing_type if segs[0] in ("self", "this") else segs[0]
    for s in segs[1:]:
        cur = field_types.get((cur, s))
        if cur is None:
            return None
    return cur

def label_for(conf, recv, tgt_type):
    if conf >= 0.94:
        return "L1", "scope: bare name resolves to a same-file symbol"
    if conf >= 0.88:
        return "L2", (f"import binding: `{recv}` is a module -> its exported symbol"
                      if recv else "import binding: a named import")
    if conf >= 0.83:
        return "L3", f"receiver type: `{recv}` is the type `{tgt_type}`"
    if conf >= 0.75:
        return "L3", f"type composition: `{recv}` -> field type `{tgt_type}` -> its method"
    return "L4", "scored disambiguation: a same-named definition won by a margin"

print()
print(f"### {label}")
bar = "-" * 78
print(bar)
calls = sorted((e for e in g["edges"] if e["kind"] == "calls"),
               key=lambda e: (nid[e["source"]].get("path", ""), nid[e["source"]]["label"]))
if not calls:
    print("  (no call edges)")

used = set()
for e in calls:
    s, t = nid[e["source"]], nid[e["target"]]
    conf = e["confidence"]
    tline = (t.get("lines") or [None])[0]
    tsid = next((sid for sid, v in sym_by_id.items()
                 if v["name"] == t["label"] and v["path"] == t.get("path")
                 and v["line"] == tline), None)
    tgt_type = sym_by_id.get(tsid, {}).get("type_name")

    cands = [r for r in refs if r["kind"] == "call" and r["name"] == t["label"]
             and sym_by_id.get(r["esid"], {}).get("name") == s["label"]
             and sym_by_id.get(r["esid"], {}).get("path") == s.get("path")]
    chosen = None
    if len(cands) == 1:
        chosen = cands[0]
    else:
        encl_type = sym_by_id.get(cands[0]["esid"], {}).get("type_name") if cands else None
        for r in cands:
            if id(r) in used:
                continue
            if tgt_type and chain_type(encl_type, r["recv"]) == tgt_type:
                chosen = r; break
            last = (r["recv"] or "").split("::")[-1].split(".")[-1]
            if tgt_type and last == tgt_type:
                chosen = r; break
        chosen = chosen or next((r for r in cands if id(r) not in used), cands[0] if cands else None)
    if chosen is not None:
        used.add(id(chosen))
    recv = chosen["recv"] if chosen else None

    L, why = label_for(conf, recv, tgt_type)
    tloc = f"@{tline}" if tline else ""
    print(f"  {s.get('path','?')}::{s['label']:<12} ->  {t.get('path','?')}::{t['label']}{tloc}")
    print(f"        [{L}] {why}   (conf {conf:.2f})")

def edge_exists(r):
    for e in calls:
        s, t = nid[e["source"]], nid[e["target"]]
        if t["label"] == r["name"] and sym_by_id.get(r["esid"], {}).get("name") == s["label"] \
           and sym_by_id.get(r["esid"], {}).get("path") == s.get("path"):
            return True
    return False

misses = []
for r in refs:
    if r["kind"] != "call" or edge_exists(r):
        continue
    encl = sym_by_id.get(r["esid"], {}).get("name", "<file>")
    if r["name"].endswith("!"):
        reason = "a macro / builtin, not a project symbol"
    elif r["loc"]:
        reason = f"`{r['name']}` binds to a local/param here -> no edge (correct)"
    elif r["name"] not in all_names:
        reason = (f"`{(r['recv'] + '.') if r['recv'] else ''}{r['name']}` is external "
                  "(no project symbol by that name)")
    else:
        reason = "ambiguous -> dropped by precision mode"
    misses.append(f"  {r['path']}::{encl} -> {r['name']}()   -- {reason}")
if misses:
    print("  no edge:")
    for m in sorted(set(misses)):
        print("  " + m)
print(bar)
'@

function Explain([string]$proj, [string]$label, [switch]$Raw) {
    $cache = Join-Path $proj '.code-rcl'
    Remove-Item -Recurse -Force $cache -ErrorAction SilentlyContinue
    $out = [System.IO.Path]::GetTempFileName()
    & $bin graph --project $proj --format json -o $out 2>&1 | Out-Null
    $db = Join-Path $cache 'cache.db'
    $pyArgs = @('-', $out, $db, $label)
    if ($Raw) { $pyArgs += 'raw' }
    $analyzer | & $py $pyArgs
    Remove-Item $out -ErrorAction SilentlyContinue
    Remove-Item -Recurse -Force $cache -ErrorAction SilentlyContinue
}

# ---------------------------------------------------------------------------
Say 'Layered reference resolution - live walkthrough'
Write-Host @'
The resolver tries layers from most-certain to most-guessy and stops at the
first that answers. The confidence on each edge tells you which layer won:

  0.95  L1  lexical scope        (bare name -> a symbol in the same file)
  0.90  L2  import binding       (named import, or ns.foo() through a module)
  0.85  L3  receiver type        (self, Foo::bar, an annotated parameter)
  0.80  L3  type composition     (self.field.method() walked via field types)
  <=.70 L4  scored disambiguation (a same-named definition wins by a margin)
'@

Build 'current branch (after)'

# ---------------------------------------------------------------------------
Say '1) docs/demo/mini_rust  -  Job::start makes four calls'
Write-Host @'
  scheduler.rs :  pub fn run()            <- a FREE function
  worker.rs    :  impl Retry { fn run() } <- a METHOD with the SAME name
  lib.rs       :  struct Job { retry: Retry }
                  fn start(&self, scheduler: u32) {
                      helper(); pick();          // bare calls
                      crate::scheduler::run();   // the free function
                      self.retry.run();          // Retry::run, via the field type
                  }
  `scheduler` is also a PARAMETER here - a bare `scheduler` would be the param,
  never the module.
'@
Explain 'docs/demo/mini_rust' 'mini_rust: resolved call edges'

# ---------------------------------------------------------------------------
Say '2) docs/demo/mini_ts  -  one method name, two classes'
Write-Host @'
  store.ts   :  class UserStore  { findById() }
                class OrderStore { findById() }
  service.ts :  class Service {
                    private users: UserStore;
                    private orders: OrderStore;
                    lookup(id) { this.users.findById(id); this.orders.findById(id); }
                }
  The declared field types make each findById call land on the right class.
'@
Explain 'docs/demo/mini_ts' 'mini_ts: resolved call edges'

# ---------------------------------------------------------------------------
if ($Before) {
    Say '3) BEFORE - the old name-only matcher (branch: main)'

    & git diff --quiet;        $dirty  = ($LASTEXITCODE -ne 0)
    & git diff --cached --quiet; if ($LASTEXITCODE -ne 0) { $dirty = $true }
    $curr = (& git rev-parse --abbrev-ref HEAD).Trim()
    $stashed = $false

    try {
        if ($dirty) {
            & git stash push -q -m 'walkthrough-before'
            if ($LASTEXITCODE -ne 0) { Write-Error 'git stash failed'; exit 1 }
            $stashed = $true
        }
        & git switch -q main
        if ($LASTEXITCODE -ne 0) { Write-Error 'cannot switch to main'; exit 1 }
        Build 'main (before)'
        Explain 'docs/demo/mini_rust' 'mini_rust @ main: name-only matching' -Raw
        Explain 'docs/demo/mini_ts'   'mini_ts @ main: name-only matching'   -Raw
    }
    finally {
        & git switch -q $curr 2>$null
        if ($stashed) { & git stash pop -q 2>$null }
        Build "restoring $curr"
    }

    Say 'Notice on main (name-only matching):'
    Write-Host '  mini_rust: start() shows just ONE run edge, to worker.rs::run (0.55) --'
    Write-Host '             it stands in for BOTH calls, and scheduler::run() is missed.'
    Write-Host '  mini_ts:   lookup() has NO edges at all -- two methods named findById'
    Write-Host '             exist, so the old heuristic refuses to pick and drops both.'
    Write-Host ''
    Write-Host '  After (this branch): 4 correct edges for start(), and lookup() resolves'
    Write-Host '  to UserStore.findById and OrderStore.findById separately.'
}

Say 'Machine-checked version:'
Write-Host '  cargo test --test resolve_accuracy -- --nocapture'
Write-Host ''