Spaces:
Running
Running
Timeline demo: precomputed GRU states, live adapter generation
Browse files- README.md +25 -119
- app.py +522 -308
- c2l/__init__.py +0 -0
- {c2l_demo → c2l}/core.py +0 -0
- {c2l_demo → c2l}/metrics.py +3 -65
- c2l_demo/__init__.py +0 -17
- c2l_demo/assertions.py +0 -360
- c2l_demo/embedding.py +0 -165
- c2l_demo/engine.py +0 -400
- c2l_demo/git_pipeline.py +0 -498
- c2l_demo/viz.py +0 -192
- requirements.txt +3 -5
- tools/build_space_data.py +92 -0
- tools/precompute.py +209 -0
- tools/precompute.sbatch +27 -0
- tools/smoke_test.py +140 -0
- tools/smoke_test.sbatch +20 -0
- upload_checkpoint.py +0 -47
README.md
CHANGED
|
@@ -1,130 +1,36 @@
|
|
| 1 |
---
|
| 2 |
-
title: Code2LoRA
|
| 3 |
emoji: 🧬
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
app_file: app.py
|
| 9 |
-
pinned:
|
| 10 |
license: mit
|
| 11 |
-
short_description:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
# Code2LoRA
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
base model
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
0. **Requirements panel** — what kind of repository works (Python + pytest /
|
| 30 |
-
unittest, real git history, assertions that grew over time).
|
| 31 |
-
1. **Repository input** — paste a public git URL (cloned with full history) or
|
| 32 |
-
an absolute path to an existing clone.
|
| 33 |
-
2. **Statistics** (first **80%** of the history):
|
| 34 |
-
- total first-parent commits,
|
| 35 |
-
- commits that touch test files,
|
| 36 |
-
- **commits selected for the GRU pass** (those introducing *new/changed*
|
| 37 |
-
assertions),
|
| 38 |
-
- number of assertion QnAs mined,
|
| 39 |
-
- chronological **80/10/10** train/val/test split,
|
| 40 |
-
- a **commit timeline** (dot size = new assertions, colored by split),
|
| 41 |
-
- the **assertion-type distribution**.
|
| 42 |
-
2.2 **GRU processing visualization** — a PCA trajectory of the GRU hidden state
|
| 43 |
-
as it streams through the repository, plus the curve of accumulated
|
| 44 |
-
"repository knowledge" feeding the adapter.
|
| 45 |
-
3. **Prediction showcase** — pick a **held-out** assertion (from the val/test
|
| 46 |
-
tail the adapter never saw), blank its right-hand side, and compare:
|
| 47 |
-
- the plain `Qwen2.5-Coder-1.5B` completion,
|
| 48 |
-
- the completion **with the Code2LoRA-GRU adapter injected**,
|
| 49 |
-
- the ground truth — with exact-match / edit-similarity badges.
|
| 50 |
-
|
| 51 |
-
---
|
| 52 |
-
|
| 53 |
-
## Architecture (one screen)
|
| 54 |
-
|
| 55 |
-
```
|
| 56 |
-
repo ──► select kept commits ──► per-commit diff embeddings (Qwen3-Embedding-0.6B, 2048-d)
|
| 57 |
-
│
|
| 58 |
-
repo-state @ commit 0 ──► h0 ──► ┌──┴── CommitGRU (O(1) / commit) ──► h_T
|
| 59 |
-
│
|
| 60 |
-
Code2LoRAHead ──► LoRA(A,B) per module type
|
| 61 |
-
│
|
| 62 |
-
injected into frozen Qwen2.5-Coder-1.5B ──► prediction
|
| 63 |
-
```
|
| 64 |
-
|
| 65 |
-
* **Commit selection / diffs / assertion mining** — `c2l_demo/git_pipeline.py`,
|
| 66 |
-
`c2l_demo/assertions.py`.
|
| 67 |
-
* **Embeddings** — `c2l_demo/embedding.py` (diff: 512/64 chunks → MaxPool‖MeanPool;
|
| 68 |
-
repo-state: 2048/256 chunks → mean‖max, L2-norm).
|
| 69 |
-
* **GRU + adapter head + LoRA injection** — `c2l_demo/core.py`.
|
| 70 |
-
* **Orchestration / generation / metrics** — `c2l_demo/engine.py`,
|
| 71 |
-
`c2l_demo/metrics.py`.
|
| 72 |
-
* **Plots** — `c2l_demo/viz.py`.
|
| 73 |
-
|
| 74 |
-
Every module is a faithful port of the corresponding file in the research
|
| 75 |
-
project (`hypernetwork/code2lora_core.py`,
|
| 76 |
-
`evaluation/run_code2lora_gru_v2_eval.py`,
|
| 77 |
-
`create_dataset/build_commit_parquet_db.py`, …) so the demo scores predictions
|
| 78 |
-
exactly the way the paper does.
|
| 79 |
-
|
| 80 |
-
---
|
| 81 |
-
|
| 82 |
-
## Configuration
|
| 83 |
-
|
| 84 |
-
The Space loads the published paper checkpoint by default — **no setup
|
| 85 |
-
required**:
|
| 86 |
-
|
| 87 |
-
* model repo: [`code2lora/code2lora-gru`](https://huggingface.co/code2lora/code2lora-gru)
|
| 88 |
-
* file: `code2lora_gru.pt` (~2.85 GB — the GRU + the LoRA-generation head)
|
| 89 |
-
|
| 90 |
-
To point at a different checkpoint, override via environment variables
|
| 91 |
-
(Space → **Settings → Variables and secrets**):
|
| 92 |
-
|
| 93 |
-
| Variable | Default | Meaning |
|
| 94 |
-
|---|---|---|
|
| 95 |
-
| `CODE2LORA_CKPT_REPO` | `code2lora/code2lora-gru` | HF model repo holding the checkpoint |
|
| 96 |
-
| `CODE2LORA_CKPT_FILE` | `code2lora_gru.pt` | filename in that repo |
|
| 97 |
-
| `CODE2LORA_CKPT` | — | *or* an absolute local path (overrides the repo) |
|
| 98 |
-
|
| 99 |
-
The base model (`Qwen/Qwen2.5-Coder-1.5B`) and the encoder
|
| 100 |
-
(`Qwen/Qwen3-Embedding-0.6B`) are pulled from the Hub automatically.
|
| 101 |
-
|
| 102 |
-
> **Hardware.** A GPU Space (T4/A10/A100 or ZeroGPU) is strongly recommended —
|
| 103 |
-
> three models (~7 GB) and live generation. It will also run on CPU, but
|
| 104 |
-
> processing a repo and generating will be slow.
|
| 105 |
-
|
| 106 |
-
---
|
| 107 |
-
|
| 108 |
-
## Deploy
|
| 109 |
-
|
| 110 |
-
The checkpoint is already on the Hub, so deploying is just pushing this folder:
|
| 111 |
-
|
| 112 |
-
```bash
|
| 113 |
-
# Create the Space and push this directory
|
| 114 |
-
huggingface-cli repo create code2lora-gru-demo --type space --space_sdk gradio
|
| 115 |
-
git init && git remote add origin https://huggingface.co/spaces/code2lora/code2lora-gru-demo
|
| 116 |
-
git add . && git commit -m "Code2LoRA-GRU demo" && git push origin main
|
| 117 |
-
# Then pick a GPU tier in the Space settings.
|
| 118 |
-
```
|
| 119 |
-
|
| 120 |
-
`upload_checkpoint.py` is provided only if you ever need to publish a *new*
|
| 121 |
-
checkpoint; the default one is ready to use.
|
| 122 |
-
|
| 123 |
-
## Run locally
|
| 124 |
-
|
| 125 |
-
```bash
|
| 126 |
-
pip install -r requirements.txt
|
| 127 |
-
# uses code2lora/code2lora-gru by default; or point at a local checkpoint:
|
| 128 |
-
# export CODE2LORA_CKPT=/abs/path/to/gru_head.best.pt
|
| 129 |
-
python app.py # serves on http://localhost:7860
|
| 130 |
-
```
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Code2LoRA Timeline
|
| 3 |
emoji: 🧬
|
| 4 |
+
colorFrom: indigo
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.50.0
|
| 8 |
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
license: mit
|
| 11 |
+
short_description: Generate a repo-specific LoRA at any commit and test it.
|
| 12 |
+
models:
|
| 13 |
+
- code2lora/code2lora-gru
|
| 14 |
+
- Qwen/Qwen2.5-Coder-1.5B
|
| 15 |
+
datasets:
|
| 16 |
+
- code2lora/code2lora-evo
|
| 17 |
---
|
| 18 |
|
| 19 |
+
# Code2LoRA: watch an adapter learn a repository, commit by commit
|
| 20 |
|
| 21 |
+
Pick one of the held-out test repositories from
|
| 22 |
+
[RepoPeftBench](https://huggingface.co/datasets/code2lora/code2lora-evo), move through its commit history,
|
| 23 |
+
generate the Code2LoRA-Evo adapter at any commit, and compare the adapted
|
| 24 |
+
Qwen2.5-Coder-1.5B with the plain base model on the test assertions that commit
|
| 25 |
+
introduced, or on your own code prefix.
|
| 26 |
|
| 27 |
+
- **Adapt** runs the hypernetwork head live on the precomputed GRU state of the
|
| 28 |
+
selected commit and injects the resulting rank-16 LoRA into the frozen base model.
|
| 29 |
+
- Cached predictions were produced on a GPU with exactly the same adapter, prompt
|
| 30 |
+
(≤ 4,096 tokens) and scoring as the paper; **Run live** regenerates them here.
|
| 31 |
+
- `tools/` contains the scripts that built `data/` from the published dataset and
|
| 32 |
+
checkpoint.
|
| 33 |
|
| 34 |
+
Paper: [arXiv 2606.06492](https://arxiv.org/abs/2606.06492) ·
|
| 35 |
+
Product: [code2lora.app](https://code2lora.app) ·
|
| 36 |
+
CLI: `pip install code2lora`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,260 +1,429 @@
|
|
| 1 |
-
"""Code2LoRA
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
2. shows repository statistics, restricted to the first 80% of the history,
|
| 8 |
-
3. visualizes how the streaming GRU walks the repository and accumulates a
|
| 9 |
-
repository-specific adapter,
|
| 10 |
-
4. lets you watch the generated LoRA change what the base code model predicts
|
| 11 |
-
for a held-out assertion.
|
| 12 |
-
|
| 13 |
-
See README.md for the architecture and deployment notes.
|
| 14 |
"""
|
| 15 |
|
| 16 |
from __future__ import annotations
|
| 17 |
|
|
|
|
| 18 |
import html
|
| 19 |
-
import
|
| 20 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
from pathlib import Path
|
| 22 |
-
from typing import List, Optional
|
| 23 |
|
| 24 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
from c2l_demo.engine import Engine, RepoSession
|
| 27 |
|
| 28 |
# ---------------------------------------------------------------------------
|
| 29 |
-
#
|
| 30 |
# ---------------------------------------------------------------------------
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
WORK_DIR.mkdir(parents=True, exist_ok=True)
|
| 35 |
-
|
| 36 |
-
EXAMPLE_REPOS = [
|
| 37 |
-
"https://github.com/psf/cachecontrol",
|
| 38 |
-
"https://github.com/john-kurkowski/tldextract",
|
| 39 |
-
"https://github.com/agronholm/anyio",
|
| 40 |
-
]
|
| 41 |
-
|
| 42 |
-
REQUIREMENTS_MD = """
|
| 43 |
-
### What kind of repository works best?
|
| 44 |
-
|
| 45 |
-
Code2LoRA-GRU learns from how a repository's **test suite** evolves, so the
|
| 46 |
-
demo needs a repo that:
|
| 47 |
-
|
| 48 |
-
- **is a Python project** with a `pytest` / `unittest` test suite,
|
| 49 |
-
- **has real git history** (clone with full history — not a shallow/zip
|
| 50 |
-
download), because the GRU streams *commit by commit*,
|
| 51 |
-
- contains **assertions that grew over time** (`assert x == ...`,
|
| 52 |
-
`self.assertEqual(...)`, `pytest.raises(...)`, `assert_allclose(...)`, …),
|
| 53 |
-
- is a **moderate size** (a few hundred commits is ideal; very large
|
| 54 |
-
monorepos will be slow on CPU).
|
| 55 |
-
|
| 56 |
-
Paste either a **public git URL** (it will be cloned) or an **absolute local
|
| 57 |
-
path** to a clone that already exists on the Space.
|
| 58 |
-
"""
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
# ---------------------------------------------------------------------------
|
| 81 |
# Rendering helpers
|
| 82 |
# ---------------------------------------------------------------------------
|
| 83 |
|
| 84 |
-
def
|
| 85 |
-
return
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
_stat_card("Total commits", f"{r.total_commits:,}",
|
| 102 |
-
"first-parent history", "#e6e6e6"),
|
| 103 |
-
_stat_card("Touch test files", f"{r.test_touching_commits:,}",
|
| 104 |
-
"commits changing a test", "#a78bfa"),
|
| 105 |
-
_stat_card("GRU-pass commits", f"{r.kept_commits:,}",
|
| 106 |
-
f"{pct_kept:.0f}% introduce new assertions", "#22d3a6"),
|
| 107 |
-
_stat_card("Assertions (QnAs)", f"{r.n_assertions:,}",
|
| 108 |
-
"prediction targets mined", "#4f8cff"),
|
| 109 |
-
])
|
| 110 |
-
split = "".join([
|
| 111 |
-
_stat_card("Train (≤80%)", f"{r.n_train_kept:,}",
|
| 112 |
-
f"walked: {boundary} commits", "#4f8cff"),
|
| 113 |
-
_stat_card("Val (10%)", f"{r.n_val_kept:,}", "held out", "#ffcc4d"),
|
| 114 |
-
_stat_card("Test (10%)", f"{r.n_test_kept:,}", "held out", "#ff6b6b"),
|
| 115 |
-
_stat_card("Repo-state files", f"{session.n_repo_state_files:,}",
|
| 116 |
-
"embedded at commit 0", "#a78bfa"),
|
| 117 |
-
])
|
| 118 |
-
return f"""
|
| 119 |
-
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">{cards}</div>
|
| 120 |
-
<div style="display:flex;gap:12px;flex-wrap:wrap;">{split}</div>
|
| 121 |
-
<p style="color:#8b93a7;font-size:13px;margin-top:12px;">
|
| 122 |
-
The adapter is built by streaming the GRU over the first
|
| 123 |
-
<b style="color:#22d3a6;">80%</b> of the
|
| 124 |
-
{r.kept_commits:,} GRU-pass commits ({boundary:,} commits). The remaining
|
| 125 |
-
val/test commits are never seen by the adapter — predictions on them are
|
| 126 |
-
genuine generalization.</p>"""
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def _badge(ok: bool) -> str:
|
| 130 |
-
if ok:
|
| 131 |
-
return ('<span style="background:#0f3d2e;color:#22d3a6;border:1px solid '
|
| 132 |
-
'#22d3a6;border-radius:6px;padding:2px 8px;font-size:12px;">'
|
| 133 |
-
'✓ exact match</span>')
|
| 134 |
-
return ('<span style="background:#3d1f1f;color:#ff6b6b;border:1px solid '
|
| 135 |
-
'#ff6b6b;border-radius:6px;padding:2px 8px;font-size:12px;">'
|
| 136 |
-
'✗ miss</span>')
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def render_prediction(res: dict) -> str:
|
| 140 |
-
prefix = res["prefix"]
|
| 141 |
-
# Show only the tail of the prefix so the card stays readable.
|
| 142 |
-
tail = prefix[-1400:]
|
| 143 |
-
if len(prefix) > len(tail):
|
| 144 |
-
tail = "…\n" + tail
|
| 145 |
-
target = res["target"]
|
| 146 |
-
base = res["base_clean"] or "(empty)"
|
| 147 |
-
adapted = res["adapted_clean"] or "(empty)"
|
| 148 |
-
|
| 149 |
-
def block(title, body, color):
|
| 150 |
-
return f"""
|
| 151 |
-
<div style="flex:1;min-width:240px;background:#161922;border:1px solid #2a2e3a;
|
| 152 |
-
border-radius:12px;padding:14px 16px;">
|
| 153 |
-
<div style="font-size:12px;color:#8b93a7;text-transform:uppercase;
|
| 154 |
-
letter-spacing:.05em;margin-bottom:6px;">{title}</div>
|
| 155 |
-
<div style="font-family:ui-monospace,Menlo,monospace;font-size:15px;
|
| 156 |
-
color:{color};word-break:break-word;">{html.escape(body)}</div>
|
| 157 |
-
</div>"""
|
| 158 |
-
|
| 159 |
-
cards = "".join([
|
| 160 |
-
block("Base Qwen2.5-Coder (no adapter)", base, "#ff9e6b") +
|
| 161 |
-
f'<div style="margin-top:8px;">{_badge(res["base_exact_match"])}'
|
| 162 |
-
f'<span style="color:#8b93a7;font-size:12px;margin-left:8px;">'
|
| 163 |
-
f'edit sim {res["base_edit_similarity"]:.2f}</span></div>',
|
| 164 |
-
block("+ Code2LoRA-GRU adapter", adapted, "#22d3a6") +
|
| 165 |
-
f'<div style="margin-top:8px;">{_badge(res["adapted_exact_match"])}'
|
| 166 |
-
f'<span style="color:#8b93a7;font-size:12px;margin-left:8px;">'
|
| 167 |
-
f'edit sim {res["adapted_edit_similarity"]:.2f}</span></div>',
|
| 168 |
-
block("Ground truth", target, "#e6e6e6"),
|
| 169 |
-
])
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
return f"""
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
|
| 205 |
# ---------------------------------------------------------------------------
|
| 206 |
-
#
|
| 207 |
# ---------------------------------------------------------------------------
|
| 208 |
|
| 209 |
-
def
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
|
| 260 |
# ---------------------------------------------------------------------------
|
|
@@ -262,92 +431,137 @@ def predict_cb(session: Optional[RepoSession], selected: Optional[str]):
|
|
| 262 |
# ---------------------------------------------------------------------------
|
| 263 |
|
| 264 |
CSS = """
|
| 265 |
-
.gradio-container {max-width: 1180px !important
|
| 266 |
-
#hero {
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
"""
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
| 278 |
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
Turn any Python repository into a parameter-efficient adapter for a
|
| 284 |
-
frozen code LLM — by <b>streaming its commit history through a GRU</b>
|
| 285 |
-
and generating LoRA weights on the fly. No fine-tuning, no retrieval
|
| 286 |
-
at inference time.
|
| 287 |
-
</div>
|
| 288 |
-
</div>""")
|
| 289 |
|
| 290 |
-
|
| 291 |
-
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
gr.
|
| 299 |
-
with gr.Row():
|
| 300 |
-
repo_input = gr.Textbox(
|
| 301 |
-
label="Git URL or local path",
|
| 302 |
-
placeholder="https://github.com/owner/repo or /abs/path/to/clone",
|
| 303 |
-
scale=4)
|
| 304 |
-
process_btn = gr.Button("Process repository", variant="primary", scale=1)
|
| 305 |
-
gr.Examples(EXAMPLE_REPOS, inputs=repo_input, label="Example repositories")
|
| 306 |
-
status = gr.HTML()
|
| 307 |
-
|
| 308 |
-
# 2) Statistics
|
| 309 |
-
gr.Markdown("## 2 · Repository statistics (first 80% of history)",
|
| 310 |
-
elem_classes="section-title")
|
| 311 |
-
stats_html = gr.HTML()
|
| 312 |
-
with gr.Row():
|
| 313 |
-
timeline_plot = gr.Plot(label="Commit timeline · dot size = new assertions")
|
| 314 |
with gr.Row():
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
if __name__ == "__main__":
|
| 353 |
-
|
|
|
|
| 1 |
+
"""Code2LoRA timeline demo.
|
| 2 |
|
| 3 |
+
Pick a held-out repository, move through its commit history, generate the
|
| 4 |
+
Code2LoRA-Evo adapter at any commit, and compare the adapted model against the
|
| 5 |
+
plain base model on the test assertions that commit introduced, or on your own
|
| 6 |
+
code prompt.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
| 11 |
+
import gzip
|
| 12 |
import html
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import threading
|
| 16 |
+
import time
|
| 17 |
+
from collections import OrderedDict
|
| 18 |
+
from functools import lru_cache
|
| 19 |
from pathlib import Path
|
| 20 |
+
from typing import Dict, List, Optional, Tuple
|
| 21 |
|
| 22 |
import gradio as gr
|
| 23 |
+
import numpy as np
|
| 24 |
+
import torch
|
| 25 |
+
from huggingface_hub import hf_hub_download
|
| 26 |
+
from matplotlib.figure import Figure
|
| 27 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, StoppingCriteria, StoppingCriteriaList
|
| 28 |
+
|
| 29 |
+
from c2l.core import (
|
| 30 |
+
Code2LoRAHead,
|
| 31 |
+
clear_all_lora_weights,
|
| 32 |
+
discover_module_types_and_dims,
|
| 33 |
+
get_module_specs,
|
| 34 |
+
inject_lora_weights,
|
| 35 |
+
replace_with_lora,
|
| 36 |
+
)
|
| 37 |
+
from c2l.metrics import compute_metrics, strip_fim_tokens
|
| 38 |
+
|
| 39 |
+
BASE_MODEL = "Qwen/Qwen2.5-Coder-1.5B"
|
| 40 |
+
CKPT_REPO, CKPT_FILE = "code2lora/code2lora-gru", "code2lora_gru.pt"
|
| 41 |
+
TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj", "up_proj", "gate_proj", "down_proj"]
|
| 42 |
+
DATA = Path(os.environ.get("C2L_DATA_DIR", Path(__file__).parent / "data"))
|
| 43 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 44 |
+
DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32
|
| 45 |
+
LIVE_MAX_INPUT = 4096 if DEVICE == "cuda" else 1024
|
| 46 |
+
PAPER_URL = "https://arxiv.org/abs/2606.06492"
|
| 47 |
+
SITE_URL = "https://code2lora.app"
|
| 48 |
+
|
| 49 |
+
torch.set_grad_enabled(False)
|
| 50 |
|
|
|
|
| 51 |
|
| 52 |
# ---------------------------------------------------------------------------
|
| 53 |
+
# Data
|
| 54 |
# ---------------------------------------------------------------------------
|
| 55 |
|
| 56 |
+
INDEX: List[dict] = json.loads((DATA / "index.json").read_text())
|
| 57 |
+
INDEX_BY_SAFE = {r["safe"]: r for r in INDEX}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
|
| 60 |
+
@lru_cache(maxsize=6)
|
| 61 |
+
def load_repo(safe: str) -> dict:
|
| 62 |
+
with gzip.open(DATA / f"{safe}.json.gz", "rt", encoding="utf-8") as f:
|
| 63 |
+
d = json.load(f)
|
| 64 |
+
d["ctx"] = np.load(DATA / f"{safe}.ctx.npy")
|
| 65 |
+
d["tt_commits"] = [k for k, c in enumerate(d["commits"]) if c.get("time_travel")]
|
| 66 |
+
return d
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def repo_label(r: dict) -> str:
|
| 70 |
+
return f"{r['repo_id']} · {r['n_commits']} commits"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# ---------------------------------------------------------------------------
|
| 74 |
+
# Model
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
|
| 77 |
+
class NewlineStop(StoppingCriteria):
|
| 78 |
+
def __init__(self, tok, start: int):
|
| 79 |
+
self.tok, self.start = tok, start
|
| 80 |
+
|
| 81 |
+
def __call__(self, input_ids, scores, **kwargs):
|
| 82 |
+
done = "\n" in self.tok.decode(input_ids[0, self.start:], skip_special_tokens=True)
|
| 83 |
+
return torch.full((input_ids.shape[0],), done, dtype=torch.bool, device=input_ids.device)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class Engine:
|
| 87 |
+
def __init__(self) -> None:
|
| 88 |
+
t0 = time.time()
|
| 89 |
+
self.tok = AutoTokenizer.from_pretrained(BASE_MODEL)
|
| 90 |
+
if self.tok.pad_token is None:
|
| 91 |
+
self.tok.pad_token = self.tok.eos_token
|
| 92 |
+
self.bos = self.tok.bos_token_id if self.tok.bos_token_id is not None else self.tok.eos_token_id
|
| 93 |
+
self.model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, torch_dtype=DTYPE).to(DEVICE).eval()
|
| 94 |
+
|
| 95 |
+
path = hf_hub_download(CKPT_REPO, CKPT_FILE)
|
| 96 |
+
try:
|
| 97 |
+
state = torch.load(path, map_location="cpu", weights_only=False, mmap=True)
|
| 98 |
+
except Exception:
|
| 99 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 100 |
+
hc = state["head_config"]
|
| 101 |
+
self.specs = get_module_specs(self.model, TARGET_MODULES)
|
| 102 |
+
type_dims = discover_module_types_and_dims(self.specs)
|
| 103 |
+
self.rank = int(hc.get("rank", 16))
|
| 104 |
+
self.head = Code2LoRAHead(input_dim=int(hc["input_dim"]), type_dims=type_dims,
|
| 105 |
+
hidden_dim=int(hc["hidden_dim"]), rank=self.rank)
|
| 106 |
+
self.head.load_state_dict(state["head_state"])
|
| 107 |
+
self.head.to(DEVICE).eval()
|
| 108 |
+
alpha = float((state.get("args") or {}).get("alpha", 32.0))
|
| 109 |
+
del state
|
| 110 |
+
replace_with_lora(self.model, self.specs, rank=self.rank, alpha=alpha)
|
| 111 |
+
self.n_adapter_params = sum(self.rank * (i + o) for i, o in type_dims.values())
|
| 112 |
+
self.lock = threading.Lock()
|
| 113 |
+
self.adapters: "OrderedDict[Tuple[str, int], dict]" = OrderedDict()
|
| 114 |
+
print(f"[engine] ready on {DEVICE} in {time.time() - t0:.0f}s", flush=True)
|
| 115 |
+
|
| 116 |
+
def adapter(self, safe: str, k: int) -> Tuple[dict, float]:
|
| 117 |
+
key = (safe, k)
|
| 118 |
+
if key in self.adapters:
|
| 119 |
+
self.adapters.move_to_end(key)
|
| 120 |
+
return self.adapters[key], 0.0
|
| 121 |
+
ctx = torch.from_numpy(load_repo(safe)["ctx"][k:k + 1]).to(DEVICE)
|
| 122 |
+
t0 = time.perf_counter()
|
| 123 |
+
out = self.head(ctx)
|
| 124 |
+
if DEVICE == "cuda":
|
| 125 |
+
torch.cuda.synchronize()
|
| 126 |
+
ms = (time.perf_counter() - t0) * 1000
|
| 127 |
+
self.adapters[key] = out
|
| 128 |
+
while len(self.adapters) > 32:
|
| 129 |
+
self.adapters.popitem(last=False)
|
| 130 |
+
return out, ms
|
| 131 |
+
|
| 132 |
+
def _ids(self, prompt: str, had_bos: bool, max_input: int) -> List[int]:
|
| 133 |
+
ids = ([self.bos] if had_bos and self.bos is not None else []) + \
|
| 134 |
+
self.tok.encode(prompt, add_special_tokens=False)
|
| 135 |
+
return ids[-max_input:]
|
| 136 |
+
|
| 137 |
+
def generate(self, prompt: str, adapter: Optional[dict], *, had_bos: bool = True,
|
| 138 |
+
max_input: int = LIVE_MAX_INPUT, max_new: int = 64,
|
| 139 |
+
stop_newline: bool = True) -> Tuple[str, float, int]:
|
| 140 |
+
ids = self._ids(prompt, had_bos, max_input)
|
| 141 |
+
x = torch.tensor([ids], device=DEVICE)
|
| 142 |
+
stop = StoppingCriteriaList([NewlineStop(self.tok, len(ids))]) if stop_newline else None
|
| 143 |
+
with self.lock:
|
| 144 |
+
t0 = time.perf_counter()
|
| 145 |
+
if adapter is None:
|
| 146 |
+
clear_all_lora_weights(self.model, self.specs)
|
| 147 |
+
else:
|
| 148 |
+
inject_lora_weights(self.model, self.specs, adapter, batch_index=0)
|
| 149 |
+
try:
|
| 150 |
+
out = self.model.generate(
|
| 151 |
+
input_ids=x, attention_mask=torch.ones_like(x), max_new_tokens=int(max_new),
|
| 152 |
+
do_sample=False, pad_token_id=self.tok.pad_token_id,
|
| 153 |
+
eos_token_id=self.tok.eos_token_id, stopping_criteria=stop, use_cache=True)
|
| 154 |
+
finally:
|
| 155 |
+
clear_all_lora_weights(self.model, self.specs)
|
| 156 |
+
sec = time.perf_counter() - t0
|
| 157 |
+
text = self.tok.decode(out[0, len(ids):].tolist(), skip_special_tokens=True)
|
| 158 |
+
return text, sec, len(ids)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
ENGINE = Engine()
|
| 162 |
|
| 163 |
|
| 164 |
# ---------------------------------------------------------------------------
|
| 165 |
# Rendering helpers
|
| 166 |
# ---------------------------------------------------------------------------
|
| 167 |
|
| 168 |
+
def esc(s) -> str:
|
| 169 |
+
return html.escape(str(s if s is not None else ""))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def short_date(ts: Optional[str]) -> str:
|
| 173 |
+
return (ts or "")[:10]
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def verdict(em: Optional[float]) -> str:
|
| 177 |
+
if em is None:
|
| 178 |
+
return ""
|
| 179 |
+
return '<span class="ok">✓ exact match</span>' if em == 1.0 else '<span class="bad">✗ wrong</span>'
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def pct(x: Optional[float]) -> str:
|
| 183 |
+
return "–" if x is None else f"{100 * x:.0f}%"
|
| 184 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
def repo_header(safe: str) -> str:
|
| 187 |
+
r = INDEX_BY_SAFE[safe]
|
| 188 |
+
stars = f"★ {r['stars']:,} · " if r.get("stars") else ""
|
| 189 |
+
desc = f"<div class='desc'>{esc(r.get('description'))}</div>" if r.get("description") else ""
|
| 190 |
return f"""
|
| 191 |
+
<div class="repo-card">
|
| 192 |
+
<div class="repo-title"><a href="https://github.com/{esc(r['repo_id'])}" target="_blank">{esc(r['repo_id'])}</a>
|
| 193 |
+
<span class="pill held">held out — never seen in training</span></div>
|
| 194 |
+
{desc}
|
| 195 |
+
<div class="repo-stats">{stars}{r['n_commits']} commits · {short_date(r['first_date'])} → {short_date(r['last_date'])}
|
| 196 |
+
· {r['n_qnas']:,} test assertions</div>
|
| 197 |
+
<div class="repo-em">Exact match across this repo's history:
|
| 198 |
+
base model <b>{pct(r['base_em'])}</b> → Code2LoRA <b class="accent">{pct(r['c2l_em'])}</b></div>
|
| 199 |
+
</div>"""
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def commit_card(safe: str, k: int, adapted_k: Optional[int]) -> str:
|
| 203 |
+
d = load_repo(safe)
|
| 204 |
+
c = d["commits"][k]
|
| 205 |
+
n = len(d["commits"])
|
| 206 |
+
url = f"https://github.com/{d['repo_id']}/commit/{c['sha']}"
|
| 207 |
+
tt = "<span class='pill star'>✨ adapter learns something new here</span>" if c.get("time_travel") else ""
|
| 208 |
+
if adapted_k == k:
|
| 209 |
+
status = "<span class='pill live'>● adapter active for this commit</span>"
|
| 210 |
+
elif adapted_k is not None:
|
| 211 |
+
status = f"<span class='pill warn'>adapter is from commit {adapted_k + 1}: press Adapt to update</span>"
|
| 212 |
+
else:
|
| 213 |
+
status = "<span class='pill idle'>no adapter yet: press Adapt</span>"
|
| 214 |
+
return f"""
|
| 215 |
+
<div class="commit-card">
|
| 216 |
+
<div class="commit-top"><b>Commit {k + 1} / {n}</b> · <a href="{url}" target="_blank"><code>{c['sha'][:10]}</code></a>
|
| 217 |
+
· {short_date(c.get('timestamp'))} · {c.get('n_new_assertions', 0)} new test assertions {tt}</div>
|
| 218 |
+
<div class="commit-status">{status}</div>
|
| 219 |
+
</div>"""
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def timeline_plot(safe: str, k: int) -> Figure:
|
| 223 |
+
d = load_repo(safe)
|
| 224 |
+
cs = d["commits"]
|
| 225 |
+
x = np.arange(1, len(cs) + 1)
|
| 226 |
+
|
| 227 |
+
def rolling(key: str) -> np.ndarray:
|
| 228 |
+
v = np.array([c[key] if c.get(key) is not None else np.nan for c in cs], dtype=float)
|
| 229 |
+
w = max(3, len(v) // 12)
|
| 230 |
+
out = np.full_like(v, np.nan)
|
| 231 |
+
for i in range(len(v)):
|
| 232 |
+
seg = v[max(0, i - w + 1): i + 1]
|
| 233 |
+
seg = seg[~np.isnan(seg)]
|
| 234 |
+
out[i] = seg.mean() * 100 if len(seg) else np.nan
|
| 235 |
+
return out
|
| 236 |
+
|
| 237 |
+
fig = Figure(figsize=(9, 2.4), dpi=110)
|
| 238 |
+
ax = fig.add_subplot(111)
|
| 239 |
+
ax.plot(x, rolling("base_em"), color="#9aa3b5", lw=1.6, label="Base model")
|
| 240 |
+
ax.plot(x, rolling("c2l_em"), color="#4f5bd5", lw=2.2, label="Code2LoRA (adapter at each commit)")
|
| 241 |
+
for t in d["tt_commits"]:
|
| 242 |
+
ax.axvline(t + 1, color="#22c3a6", alpha=0.18, lw=1)
|
| 243 |
+
ax.axvline(k + 1, color="#ff6b8b", lw=2)
|
| 244 |
+
ax.set_xlim(1, max(2, len(cs)))
|
| 245 |
+
ax.set_ylim(0, 100)
|
| 246 |
+
ax.set_ylabel("Exact match %\n(rolling)")
|
| 247 |
+
ax.set_xlabel("Commit")
|
| 248 |
+
for s in ("top", "right"):
|
| 249 |
+
ax.spines[s].set_visible(False)
|
| 250 |
+
ax.legend(loc="lower right", fontsize=8, frameon=False)
|
| 251 |
+
fig.tight_layout()
|
| 252 |
+
return fig
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def assertion_choices(safe: str, k: int) -> List[Tuple[str, int]]:
|
| 256 |
+
qs = load_repo(safe)["qnas"][k]
|
| 257 |
+
out = []
|
| 258 |
+
for i, q in enumerate(qs):
|
| 259 |
+
star = "✨ " if q.get("prev_em") == 0.0 else ""
|
| 260 |
+
out.append((f"{star}{Path(q['test_file']).name}:{q['lineno']} {q.get('test_function') or ''}", i))
|
| 261 |
+
return out
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def context_tail(prompt: str, n_lines: int = 28) -> str:
|
| 265 |
+
return "\n".join(prompt.rstrip("\n").split("\n")[-n_lines:])
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def pred_row(label: str, cls: str, pred: Optional[str], em: Optional[float], note: str = "") -> str:
|
| 269 |
+
if pred is None:
|
| 270 |
+
return ""
|
| 271 |
+
shown = strip_fim_tokens(pred).split("\n")[0]
|
| 272 |
+
return f"""<tr class="{cls}"><td class="who">{label}</td><td><code>{esc(shown) or '∅'}</code></td>
|
| 273 |
+
<td>{verdict(em)}</td><td class="note">{note}</td></tr>"""
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def assertion_panel(safe: str, k: int, i: Optional[int], adapted_k: Optional[int]) -> Tuple[str, str]:
|
| 277 |
+
qs = load_repo(safe)["qnas"][k]
|
| 278 |
+
if i is None or i >= len(qs):
|
| 279 |
+
return "", "<div class='hint'>Pick an assertion above.</div>"
|
| 280 |
+
q = qs[i]
|
| 281 |
+
rows = [f"""<tr class="truth"><td class="who">Ground truth</td><td><code>{esc(q['target'])}</code></td><td></td>
|
| 282 |
+
<td class="note">what the developer actually wrote</td></tr>""",
|
| 283 |
+
pred_row("Base model", "base", q["base_pred"], q["base_em"], "Qwen2.5-Coder-1.5B, no adapter")]
|
| 284 |
+
if adapted_k == k:
|
| 285 |
+
if q.get("prev_pred") is not None:
|
| 286 |
+
rows.append(pred_row(f"Adapter @ commit {k}", "prev", q["prev_pred"], q["prev_em"],
|
| 287 |
+
"one commit earlier"))
|
| 288 |
+
rows.append(pred_row(f"Code2LoRA @ commit {k + 1}", "c2l", q["c2l_pred"], q["c2l_em"],
|
| 289 |
+
"adapter generated from history up to this commit"))
|
| 290 |
+
foot = ("<div class='hint'>Cached results were computed on a GPU with the exact adapter you just "
|
| 291 |
+
f"generated ({q['n_prompt_tokens']:,}-token context). Press <b>Run live</b> to regenerate on this Space.</div>")
|
| 292 |
+
else:
|
| 293 |
+
rows.append("<tr class='c2l'><td class='who'>Code2LoRA</td><td colspan='3' class='hint'>"
|
| 294 |
+
"Press <b>Adapt at this commit</b> to generate the adapter and reveal its answer.</td></tr>")
|
| 295 |
+
foot = ""
|
| 296 |
+
return context_tail(q["prompt"]), f"<table class='preds'>{''.join(rows)}</table>{foot}"
|
| 297 |
|
| 298 |
|
| 299 |
# ---------------------------------------------------------------------------
|
| 300 |
+
# Event handlers
|
| 301 |
# ---------------------------------------------------------------------------
|
| 302 |
|
| 303 |
+
def default_commit(safe: str) -> int:
|
| 304 |
+
d = load_repo(safe)
|
| 305 |
+
return d["tt_commits"][0] if d["tt_commits"] else len(d["commits"]) - 1
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def on_repo(safe: str):
|
| 309 |
+
d = load_repo(safe)
|
| 310 |
+
k = default_commit(safe)
|
| 311 |
+
choices = assertion_choices(safe, k)
|
| 312 |
+
first = choices[0][1] if choices else None
|
| 313 |
+
ctx, preds = assertion_panel(safe, k, first, None)
|
| 314 |
+
return (repo_header(safe),
|
| 315 |
+
gr.Slider(minimum=1, maximum=len(d["commits"]), value=k + 1, step=1),
|
| 316 |
+
commit_card(safe, k, None), timeline_plot(safe, k),
|
| 317 |
+
d["commits"][k].get("diff_preview") or "",
|
| 318 |
+
gr.Radio(choices=choices, value=first), ctx, preds, None, "", "")
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def on_commit(safe: str, k1: int, adapted: Optional[Tuple[str, int]]):
|
| 322 |
+
k = int(k1) - 1
|
| 323 |
+
adapted_k = adapted[1] if adapted and adapted[0] == safe else None
|
| 324 |
+
d = load_repo(safe)
|
| 325 |
+
choices = assertion_choices(safe, k)
|
| 326 |
+
first = next((v for lbl, v in choices if lbl.startswith("✨")), choices[0][1] if choices else None)
|
| 327 |
+
ctx, preds = assertion_panel(safe, k, first, adapted_k)
|
| 328 |
+
return (commit_card(safe, k, adapted_k), timeline_plot(safe, k),
|
| 329 |
+
d["commits"][k].get("diff_preview") or "",
|
| 330 |
+
gr.Radio(choices=choices, value=first), ctx, preds, "")
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def step(safe: str, k1: int, delta: int) -> int:
|
| 334 |
+
n = len(load_repo(safe)["commits"])
|
| 335 |
+
return int(min(max(1, int(k1) + delta), n))
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def next_star(safe: str, k1: int) -> int:
|
| 339 |
+
tt = load_repo(safe)["tt_commits"]
|
| 340 |
+
k = int(k1) - 1
|
| 341 |
+
later = [t for t in tt if t > k]
|
| 342 |
+
return (later[0] if later else (tt[0] if tt else k)) + 1
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def on_adapt(safe: str, k1: int, i: Optional[int]):
|
| 346 |
+
k = int(k1) - 1
|
| 347 |
+
out, ms = ENGINE.adapter(safe, k)
|
| 348 |
+
norms = {t: float(out["B"][t][0].norm() * out["A"][t][0].norm()) for t in sorted(out["A"])}
|
| 349 |
+
top = max(norms.values()) or 1.0
|
| 350 |
+
bars = "".join(f"<div class='bar'><span>{t}</span><i style='width:{100 * v / top:.0f}%'></i></div>"
|
| 351 |
+
for t, v in norms.items())
|
| 352 |
+
timing = "served from cache" if ms == 0.0 else f"generated in {ms:.0f} ms on {DEVICE.upper()}"
|
| 353 |
+
status = f"""
|
| 354 |
+
<div class="adapt-card">
|
| 355 |
+
<div class="adapt-head">⚡ Adapter for commit {k + 1} {timing}</div>
|
| 356 |
+
<div class="adapt-sub">{ENGINE.n_adapter_params:,} LoRA parameters (rank {ENGINE.rank}) for 7 projection types,
|
| 357 |
+
shared across all 28 layers · no training, no retrieved context</div>
|
| 358 |
+
<div class="bars">{bars}</div>
|
| 359 |
+
</div>"""
|
| 360 |
+
ctx, preds = assertion_panel(safe, k, i, k)
|
| 361 |
+
return status, (safe, k), commit_card(safe, k, k), ctx, preds
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def on_pick(safe: str, k1: int, i: Optional[int], adapted: Optional[Tuple[str, int]]):
|
| 365 |
+
k = int(k1) - 1
|
| 366 |
+
adapted_k = adapted[1] if adapted and adapted[0] == safe else None
|
| 367 |
+
ctx, preds = assertion_panel(safe, k, i, adapted_k)
|
| 368 |
+
return ctx, preds, ""
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def live_table(rows: List[Tuple[str, str, str, Optional[dict], float]], target: Optional[str]) -> str:
|
| 372 |
+
body = []
|
| 373 |
+
for label, cls, pred, m, sec in rows:
|
| 374 |
+
shown = (m["pred_clean"] if m else strip_fim_tokens(pred).split("\n")[0])
|
| 375 |
+
body.append(f"""<tr class="{cls}"><td class="who">{label}</td><td><code>{esc(shown) or '∅'}</code></td>
|
| 376 |
+
<td>{verdict(float(m['exact_match'])) if m else ''}</td><td class="note">{sec:.1f}s</td></tr>""")
|
| 377 |
+
truth = (f"<tr class='truth'><td class='who'>Ground truth</td><td><code>{esc(target)}</code></td>"
|
| 378 |
+
f"<td></td><td></td></tr>") if target else ""
|
| 379 |
+
return f"<div class='live-head'>Live on this Space ({DEVICE.upper()})</div><table class='preds'>{truth}{''.join(body)}</table>"
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def on_live(safe: str, k1: int, i: Optional[int], adapted: Optional[Tuple[str, int]],
|
| 383 |
+
progress=gr.Progress()):
|
| 384 |
+
k = int(k1) - 1
|
| 385 |
+
if not adapted or adapted != (safe, k):
|
| 386 |
+
return "<div class='hint warn'>Press <b>Adapt at this commit</b> first.</div>"
|
| 387 |
+
if i is None:
|
| 388 |
+
return "<div class='hint warn'>Pick an assertion first.</div>"
|
| 389 |
+
q = load_repo(safe)["qnas"][k][i]
|
| 390 |
+
adapter, _ = ENGINE.adapter(safe, k)
|
| 391 |
+
progress(0.1, desc="Base model…")
|
| 392 |
+
b, bs, n = ENGINE.generate(q["prompt"], None, had_bos=q.get("had_bos", True))
|
| 393 |
+
progress(0.55, desc="Code2LoRA adapter…")
|
| 394 |
+
a, as_, _ = ENGINE.generate(q["prompt"], adapter, had_bos=q.get("had_bos", True))
|
| 395 |
+
note = "" if n >= q["n_prompt_tokens"] else (
|
| 396 |
+
f"<div class='hint'>CPU mode uses the last {n:,} of {q['n_prompt_tokens']:,} context tokens, "
|
| 397 |
+
"so live answers can differ from the cached GPU run.</div>")
|
| 398 |
+
return live_table([("Base model", "base", b, compute_metrics(b, q["target"]), bs),
|
| 399 |
+
(f"Code2LoRA @ commit {k + 1}", "c2l", a, compute_metrics(a, q["target"]), as_)],
|
| 400 |
+
q["target"]) + note
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def on_prefill(safe: str, k1: int, i: Optional[int]) -> str:
|
| 404 |
+
qs = load_repo(safe)["qnas"][int(k1) - 1]
|
| 405 |
+
if i is None or i >= len(qs):
|
| 406 |
+
return ""
|
| 407 |
+
return context_tail(qs[i]["prompt"], 40)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
def on_custom(safe: str, k1: int, prompt: str, max_new: int,
|
| 411 |
+
adapted: Optional[Tuple[str, int]], progress=gr.Progress()):
|
| 412 |
+
k = int(k1) - 1
|
| 413 |
+
if not prompt.strip():
|
| 414 |
+
return "<div class='hint warn'>Type or paste a code prefix first.</div>"
|
| 415 |
+
if not adapted or adapted != (safe, k):
|
| 416 |
+
return "<div class='hint warn'>Press <b>Adapt at this commit</b> first.</div>"
|
| 417 |
+
adapter, _ = ENGINE.adapter(safe, k)
|
| 418 |
+
progress(0.1, desc="Base model…")
|
| 419 |
+
b, bs, _ = ENGINE.generate(prompt, None, max_new=max_new, stop_newline=False)
|
| 420 |
+
progress(0.55, desc="Code2LoRA adapter…")
|
| 421 |
+
a, as_, _ = ENGINE.generate(prompt, adapter, max_new=max_new, stop_newline=False)
|
| 422 |
+
rows = []
|
| 423 |
+
for label, cls, text, sec in (("Base model", "base", b, bs), (f"Code2LoRA @ commit {k + 1}", "c2l", a, as_)):
|
| 424 |
+
rows.append(f"<div class='gen {cls}'><div class='who'>{label} · {sec:.1f}s</div>"
|
| 425 |
+
f"<pre>{esc(strip_fim_tokens(text))}</pre></div>")
|
| 426 |
+
return f"<div class='live-head'>Live on this Space ({DEVICE.upper()})</div><div class='gens'>{''.join(rows)}</div>"
|
| 427 |
|
| 428 |
|
| 429 |
# ---------------------------------------------------------------------------
|
|
|
|
| 431 |
# ---------------------------------------------------------------------------
|
| 432 |
|
| 433 |
CSS = """
|
| 434 |
+
.gradio-container {max-width: 1180px !important}
|
| 435 |
+
#hero h1 {font-size: 2rem; margin-bottom: 4px; letter-spacing: -0.02em}
|
| 436 |
+
#hero p {color: #5a6275; margin: 0}
|
| 437 |
+
.repo-card, .commit-card, .adapt-card {border: 1px solid #e3e6ef; border-radius: 12px; padding: 14px 16px; background: #fff}
|
| 438 |
+
.repo-title {font-size: 1.15rem; font-weight: 700}
|
| 439 |
+
.repo-title a, .commit-top a {color: #2b3487; text-decoration: none}
|
| 440 |
+
.desc {color: #5a6275; margin: 4px 0}
|
| 441 |
+
.repo-stats, .commit-status {color: #6b7287; font-size: 0.9rem; margin-top: 4px}
|
| 442 |
+
.repo-em {margin-top: 8px}
|
| 443 |
+
.accent {color: #4f5bd5}
|
| 444 |
+
.pill {display: inline-block; font-size: 0.75rem; font-weight: 600; padding: 2px 9px; border-radius: 999px; margin-left: 6px; vertical-align: middle}
|
| 445 |
+
.pill.held {background: #eef0ff; color: #3d4ee0}
|
| 446 |
+
.pill.star {background: #e2f8f3; color: #0d8a75}
|
| 447 |
+
.pill.live {background: #e2f8f3; color: #0d8a75; margin-left: 0}
|
| 448 |
+
.pill.warn {background: #fff4e0; color: #a35f00; margin-left: 0}
|
| 449 |
+
.pill.idle {background: #f1f2f6; color: #6b7287; margin-left: 0}
|
| 450 |
+
.adapt-card {background: linear-gradient(135deg, #f4f5ff, #effbf8)}
|
| 451 |
+
.adapt-head {font-weight: 700; font-size: 1.05rem}
|
| 452 |
+
.adapt-sub {color: #5a6275; font-size: 0.88rem; margin: 2px 0 10px}
|
| 453 |
+
.bars .bar {display: flex; align-items: center; gap: 8px; font-size: 0.78rem; color: #5a6275; margin: 3px 0}
|
| 454 |
+
.bars .bar span {width: 76px; font-family: monospace}
|
| 455 |
+
.bars .bar i {height: 8px; border-radius: 4px; background: linear-gradient(90deg, #6d7cff, #22d3b6)}
|
| 456 |
+
table.preds {width: 100%; border-collapse: collapse; margin-top: 6px}
|
| 457 |
+
table.preds td {padding: 8px 10px; border-bottom: 1px solid #eef0f5; vertical-align: top}
|
| 458 |
+
table.preds td.who {width: 170px; font-weight: 600; white-space: nowrap}
|
| 459 |
+
table.preds td.note {color: #8a91a5; font-size: 0.82rem}
|
| 460 |
+
table.preds tr.truth {background: #f7f8fb}
|
| 461 |
+
table.preds tr.c2l {background: #f4f5ff}
|
| 462 |
+
table.preds tr.prev {background: #fffaf2}
|
| 463 |
+
table.preds code {font-size: 0.92rem; white-space: pre-wrap; word-break: break-word}
|
| 464 |
+
.ok {color: #0d8a75; font-weight: 700; white-space: nowrap}
|
| 465 |
+
.bad {color: #d6455e; font-weight: 700; white-space: nowrap}
|
| 466 |
+
.hint {color: #6b7287; font-size: 0.86rem; margin-top: 8px}
|
| 467 |
+
.hint.warn {color: #a35f00}
|
| 468 |
+
.live-head {font-weight: 700; margin: 6px 0 2px}
|
| 469 |
+
.gens {display: grid; grid-template-columns: 1fr 1fr; gap: 12px}
|
| 470 |
+
.gen {border: 1px solid #e3e6ef; border-radius: 10px; padding: 10px}
|
| 471 |
+
.gen.c2l {background: #f4f5ff}
|
| 472 |
+
.gen pre {white-space: pre-wrap; font-size: 0.85rem; margin: 6px 0 0}
|
| 473 |
"""
|
| 474 |
|
| 475 |
+
INTRO = f"""
|
| 476 |
+
<div id="hero">
|
| 477 |
+
<h1>Code2LoRA: watch an adapter learn a repository, commit by commit</h1>
|
| 478 |
+
<p>Pick a repository the model never saw during training, move through its git history, and generate the
|
| 479 |
+
repository-specific LoRA adapter at any point in time. Then compare the adapted model with the plain base model
|
| 480 |
+
on the test assertions that commit introduced, or on your own code.
|
| 481 |
+
<a href="{PAPER_URL}" target="_blank">Paper</a> · <a href="{SITE_URL}" target="_blank">code2lora.app</a> ·
|
| 482 |
+
<a href="https://pypi.org/project/code2lora/" target="_blank">pip install code2lora</a></p>
|
| 483 |
+
</div>
|
| 484 |
+
"""
|
| 485 |
|
| 486 |
+
HOW = """
|
| 487 |
+
**How it works.** For every commit, the production-code diff was embedded once with a frozen
|
| 488 |
+
Qwen3-Embedding-0.6B and folded into a GRU state (precomputed for this demo, 8 KB per commit).
|
| 489 |
+
**Adapt** runs the Code2LoRA hypernetwork head live on that state and emits a rank-16 LoRA for all attention and
|
| 490 |
+
MLP projections of the frozen Qwen2.5-Coder-1.5B. The base model never sees retrieved context: everything it
|
| 491 |
+
knows about the repository comes from the adapter.
|
| 492 |
|
| 493 |
+
**What you see.** Each commit's test assertions are the ones the developer added in that commit, scored
|
| 494 |
+
exactly as in the paper (first ≤ 8 per commit, exact match after normalization). ✨ marks commits where the
|
| 495 |
+
adapter gets an assertion right that the adapter from one commit earlier got wrong.
|
| 496 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
|
| 498 |
+
choices = [(repo_label(r), r["safe"]) for r in INDEX]
|
| 499 |
+
start = max(INDEX, key=lambda r: (r["c2l_em"] - r["base_em"]))["safe"]
|
| 500 |
|
| 501 |
+
with gr.Blocks(css=CSS, title="Code2LoRA timeline demo", theme=gr.themes.Soft(primary_hue="indigo")) as demo:
|
| 502 |
+
adapted_state = gr.State(None)
|
| 503 |
+
gr.HTML(INTRO)
|
| 504 |
+
with gr.Row():
|
| 505 |
+
repo_dd = gr.Dropdown(choices=choices, value=start, label="Repository (held out from training)", scale=3)
|
| 506 |
+
repo_html = gr.HTML()
|
| 507 |
|
| 508 |
+
with gr.Group():
|
| 509 |
+
commit_slider = gr.Slider(1, 2, value=1, step=1, label="Commit")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
with gr.Row():
|
| 511 |
+
prev_btn = gr.Button("◀ Previous", size="sm")
|
| 512 |
+
next_btn = gr.Button("Next ▶", size="sm")
|
| 513 |
+
star_btn = gr.Button("Jump to next ✨ commit", size="sm")
|
| 514 |
+
adapt_btn = gr.Button("⚡ Adapt at this commit", variant="primary", size="sm")
|
| 515 |
+
commit_html = gr.HTML()
|
| 516 |
+
plot = gr.Plot(show_label=False)
|
| 517 |
+
adapt_html = gr.HTML()
|
| 518 |
+
|
| 519 |
+
with gr.Tabs():
|
| 520 |
+
with gr.Tab("Test assertions from this commit"):
|
| 521 |
+
pick = gr.Radio(label="Assertions added in this commit", choices=[])
|
| 522 |
+
with gr.Row():
|
| 523 |
+
with gr.Column(scale=5):
|
| 524 |
+
ctx_code = gr.Code(label="Test file, right before the assertion's expected value",
|
| 525 |
+
language="python", interactive=False, lines=18)
|
| 526 |
+
with gr.Column(scale=6):
|
| 527 |
+
preds_html = gr.HTML()
|
| 528 |
+
live_btn = gr.Button("Run live on this Space")
|
| 529 |
+
live_html = gr.HTML()
|
| 530 |
+
with gr.Tab("Your own prompt"):
|
| 531 |
+
gr.Markdown("Paste a code prefix (for example a test ending in `assert result ==`). "
|
| 532 |
+
"Both models continue it; the adapter is the one generated for the selected commit.")
|
| 533 |
+
prefill_btn = gr.Button("Prefill with the selected assertion's context", size="sm")
|
| 534 |
+
custom_in = gr.Code(language="python", lines=14, label="Code prefix")
|
| 535 |
+
max_new = gr.Slider(8, 128, value=48, step=8, label="Max new tokens")
|
| 536 |
+
custom_btn = gr.Button("Generate with base model and Code2LoRA", variant="primary")
|
| 537 |
+
custom_out = gr.HTML()
|
| 538 |
+
with gr.Tab("Commit diff"):
|
| 539 |
+
diff_code = gr.Code(label="Production-code diff (first 6,000 characters)", interactive=False, lines=24)
|
| 540 |
+
with gr.Tab("About"):
|
| 541 |
+
gr.Markdown(HOW + ("\n\n*Running on CPU: live generation uses the last "
|
| 542 |
+
f"{LIVE_MAX_INPUT:,} context tokens and takes roughly 10–40 s.*"
|
| 543 |
+
if DEVICE == "cpu" else ""))
|
| 544 |
+
|
| 545 |
+
repo_outputs = [repo_html, commit_slider, commit_html, plot, diff_code, pick, ctx_code, preds_html,
|
| 546 |
+
adapted_state, adapt_html, live_html]
|
| 547 |
+
commit_outputs = [commit_html, plot, diff_code, pick, ctx_code, preds_html, live_html]
|
| 548 |
+
|
| 549 |
+
demo.load(on_repo, repo_dd, repo_outputs)
|
| 550 |
+
repo_dd.change(on_repo, repo_dd, repo_outputs)
|
| 551 |
+
commit_slider.release(on_commit, [repo_dd, commit_slider, adapted_state], commit_outputs)
|
| 552 |
+
prev_btn.click(lambda s, k: step(s, k, -1), [repo_dd, commit_slider], commit_slider).then(
|
| 553 |
+
on_commit, [repo_dd, commit_slider, adapted_state], commit_outputs)
|
| 554 |
+
next_btn.click(lambda s, k: step(s, k, 1), [repo_dd, commit_slider], commit_slider).then(
|
| 555 |
+
on_commit, [repo_dd, commit_slider, adapted_state], commit_outputs)
|
| 556 |
+
star_btn.click(next_star, [repo_dd, commit_slider], commit_slider).then(
|
| 557 |
+
on_commit, [repo_dd, commit_slider, adapted_state], commit_outputs)
|
| 558 |
+
adapt_btn.click(on_adapt, [repo_dd, commit_slider, pick],
|
| 559 |
+
[adapt_html, adapted_state, commit_html, ctx_code, preds_html], concurrency_limit=1)
|
| 560 |
+
pick.change(on_pick, [repo_dd, commit_slider, pick, adapted_state], [ctx_code, preds_html, live_html])
|
| 561 |
+
live_btn.click(on_live, [repo_dd, commit_slider, pick, adapted_state], live_html, concurrency_limit=1)
|
| 562 |
+
prefill_btn.click(on_prefill, [repo_dd, commit_slider, pick], custom_in)
|
| 563 |
+
custom_btn.click(on_custom, [repo_dd, commit_slider, custom_in, max_new, adapted_state], custom_out,
|
| 564 |
+
concurrency_limit=1)
|
| 565 |
|
| 566 |
if __name__ == "__main__":
|
| 567 |
+
demo.queue(max_size=32).launch()
|
c2l/__init__.py
ADDED
|
File without changes
|
{c2l_demo → c2l}/core.py
RENAMED
|
File without changes
|
{c2l_demo → c2l}/metrics.py
RENAMED
|
@@ -1,23 +1,10 @@
|
|
| 1 |
-
"""
|
|
|
|
| 2 |
|
| 3 |
-
Faithful port of ``evaluation/metrics.py`` (exact match / edit similarity /
|
| 4 |
-
postprocessing) so the demo scores predictions exactly the way the paper does.
|
| 5 |
-
The optional ``codebleu`` dependency is not required; a tokenized BLEU
|
| 6 |
-
fallback is used when it is absent.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import re
|
| 12 |
from difflib import SequenceMatcher
|
| 13 |
|
| 14 |
FIM_TOKENS = ("<|fim_prefix|>", "<|fim_suffix|>", "<|fim_middle|>")
|
| 15 |
|
| 16 |
-
_CODE_TOKEN_RE = re.compile(
|
| 17 |
-
r"[a-zA-Z_]\w*|0[xXoObB][\da-fA-F_]+|\d[\d_]*\.?\d*[eE]?[\d_]*"
|
| 18 |
-
r"|\"(?:[^\"\\]|\\.)*\"|'(?:[^'\\]|\\.)*'|[^\s]"
|
| 19 |
-
)
|
| 20 |
-
|
| 21 |
|
| 22 |
def strip_fim_tokens(s: str) -> str:
|
| 23 |
for tok in FIM_TOKENS:
|
|
@@ -43,42 +30,6 @@ def edit_similarity(pred: str, ref: str) -> float:
|
|
| 43 |
return SequenceMatcher(None, pred, ref).ratio()
|
| 44 |
|
| 45 |
|
| 46 |
-
def _tokenize_code(s: str):
|
| 47 |
-
return _CODE_TOKEN_RE.findall(s)
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def _ngram_bleu(pred_tokens, ref_tokens, max_n: int = 4) -> float:
|
| 51 |
-
from collections import Counter
|
| 52 |
-
import math
|
| 53 |
-
|
| 54 |
-
if not pred_tokens or not ref_tokens:
|
| 55 |
-
return 0.0
|
| 56 |
-
brevity = min(1.0, len(pred_tokens) / len(ref_tokens))
|
| 57 |
-
log_bleu = 0.0
|
| 58 |
-
for n in range(1, max_n + 1):
|
| 59 |
-
pred_ngrams = Counter(
|
| 60 |
-
tuple(pred_tokens[i:i + n]) for i in range(len(pred_tokens) - n + 1))
|
| 61 |
-
ref_ngrams = Counter(
|
| 62 |
-
tuple(ref_tokens[i:i + n]) for i in range(len(ref_tokens) - n + 1))
|
| 63 |
-
matches = sum(min(pred_ngrams[ng], ref_ngrams[ng]) for ng in pred_ngrams)
|
| 64 |
-
total = max(len(pred_tokens) - n + 1, 1)
|
| 65 |
-
precision = (matches + 1) / (total + 1)
|
| 66 |
-
log_bleu += math.log(precision) / max_n
|
| 67 |
-
return brevity * math.exp(log_bleu)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def code_bleu_score(pred: str, ref: str, lang: str = "python") -> float:
|
| 71 |
-
if not pred.strip() or not ref.strip():
|
| 72 |
-
return 0.0
|
| 73 |
-
if pred.strip() == ref.strip():
|
| 74 |
-
return 1.0
|
| 75 |
-
try:
|
| 76 |
-
from codebleu import calc_codebleu
|
| 77 |
-
return calc_codebleu([ref], [pred], lang=lang)["codebleu"]
|
| 78 |
-
except Exception:
|
| 79 |
-
return _ngram_bleu(_tokenize_code(pred), _tokenize_code(ref))
|
| 80 |
-
|
| 81 |
-
|
| 82 |
def postprocess_prediction(pred: str, target: str) -> str:
|
| 83 |
pred = strip_fim_tokens(pred)
|
| 84 |
if "\n" not in target and "\n" in pred:
|
|
@@ -97,20 +48,7 @@ def compute_metrics(pred: str, ref: str) -> dict:
|
|
| 97 |
pred_clean = postprocess_prediction(pred, ref)
|
| 98 |
ref_clean = strip_comments(ref)
|
| 99 |
return {
|
|
|
|
| 100 |
"exact_match": exact_match(pred_clean, ref_clean),
|
| 101 |
"edit_similarity": edit_similarity(pred_clean, ref_clean),
|
| 102 |
-
"code_bleu": code_bleu_score(pred_clean, ref_clean),
|
| 103 |
-
"pred_clean": pred_clean,
|
| 104 |
-
"ref_clean": ref_clean,
|
| 105 |
}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
__all__ = [
|
| 109 |
-
"compute_metrics",
|
| 110 |
-
"postprocess_prediction",
|
| 111 |
-
"exact_match",
|
| 112 |
-
"edit_similarity",
|
| 113 |
-
"code_bleu_score",
|
| 114 |
-
"normalize_for_match",
|
| 115 |
-
"strip_comments",
|
| 116 |
-
]
|
|
|
|
| 1 |
+
"""Assertion-completion scoring, identical to RepoPeftBench's evaluation/metrics.py
|
| 2 |
+
(exact match and edit similarity; CodeBLEU omitted)."""
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from difflib import SequenceMatcher
|
| 5 |
|
| 6 |
FIM_TOKENS = ("<|fim_prefix|>", "<|fim_suffix|>", "<|fim_middle|>")
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def strip_fim_tokens(s: str) -> str:
|
| 10 |
for tok in FIM_TOKENS:
|
|
|
|
| 30 |
return SequenceMatcher(None, pred, ref).ratio()
|
| 31 |
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
def postprocess_prediction(pred: str, target: str) -> str:
|
| 34 |
pred = strip_fim_tokens(pred)
|
| 35 |
if "\n" not in target and "\n" in pred:
|
|
|
|
| 48 |
pred_clean = postprocess_prediction(pred, ref)
|
| 49 |
ref_clean = strip_comments(ref)
|
| 50 |
return {
|
| 51 |
+
"pred_clean": pred_clean,
|
| 52 |
"exact_match": exact_match(pred_clean, ref_clean),
|
| 53 |
"edit_similarity": edit_similarity(pred_clean, ref_clean),
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c2l_demo/__init__.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
"""Code2LoRA-GRU interactive demo package.
|
| 2 |
-
|
| 3 |
-
Self-contained re-packaging of the Code2LoRA-GRU inference pipeline so it can
|
| 4 |
-
run as a standalone Hugging Face Space without depending on the research
|
| 5 |
-
monorepo. Every module here is a faithful port of the corresponding file in
|
| 6 |
-
the `RepoPeftData` project; see each module docstring for provenance.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
__all__ = [
|
| 10 |
-
"core",
|
| 11 |
-
"assertions",
|
| 12 |
-
"git_pipeline",
|
| 13 |
-
"embedding",
|
| 14 |
-
"metrics",
|
| 15 |
-
"engine",
|
| 16 |
-
"viz",
|
| 17 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c2l_demo/assertions.py
DELETED
|
@@ -1,360 +0,0 @@
|
|
| 1 |
-
"""Assertion extraction from Python test files.
|
| 2 |
-
|
| 3 |
-
Faithful port of the AST-based assertion parsers in
|
| 4 |
-
``create_dataset/create_qnas.py`` plus the full-file-prefix extractor
|
| 5 |
-
``extract_assertions_from_source`` from
|
| 6 |
-
``create_dataset/build_commit_parquet_db.py``.
|
| 7 |
-
|
| 8 |
-
A QnA pair is one assertion turned into (prefix, target):
|
| 9 |
-
|
| 10 |
-
* ``prefix`` -- the full test-file content up to (but not including) the
|
| 11 |
-
assertion's right-hand side, plus the in-line prefix (e.g. ``assert x == ``).
|
| 12 |
-
* ``target`` -- the right-hand side the model must predict (e.g. ``42``).
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
from __future__ import annotations
|
| 16 |
-
|
| 17 |
-
import ast
|
| 18 |
-
import re
|
| 19 |
-
import warnings
|
| 20 |
-
from dataclasses import dataclass
|
| 21 |
-
from typing import List, Optional
|
| 22 |
-
|
| 23 |
-
SELF_ASSERT_METHODS = {
|
| 24 |
-
"assertEqual", "assertNotEqual", "assertTrue", "assertFalse",
|
| 25 |
-
"assertIs", "assertIsNot", "assertIsNone", "assertIsNotNone",
|
| 26 |
-
"assertIn", "assertNotIn", "assertIsInstance", "assertNotIsInstance",
|
| 27 |
-
"assertRaises", "assertRaisesRegex", "assertAlmostEqual", "assertNotAlmostEqual",
|
| 28 |
-
"assertDictEqual", "assertListEqual", "assertTupleEqual", "assertSetEqual",
|
| 29 |
-
"assertSequenceEqual", "assertRegex", "assertNotRegex", "assertCountEqual",
|
| 30 |
-
"assertWarns", "assertLogs", "assertMultiLineEqual",
|
| 31 |
-
"assertGreater", "assertGreaterEqual", "assertLess", "assertLessEqual",
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
@dataclass
|
| 36 |
-
class AssertionPair:
|
| 37 |
-
assertion_type: str
|
| 38 |
-
prefix: str
|
| 39 |
-
target: str
|
| 40 |
-
lineno: int
|
| 41 |
-
col_offset: int
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def get_source_segment(source: str, node: ast.AST) -> Optional[str]:
|
| 45 |
-
if hasattr(ast, "get_source_segment"):
|
| 46 |
-
return ast.get_source_segment(source, node)
|
| 47 |
-
lines = source.splitlines()
|
| 48 |
-
start = getattr(node, "lineno", 1) - 1
|
| 49 |
-
end = getattr(node, "end_lineno", start + 1) - 1
|
| 50 |
-
if start < 0 or end >= len(lines):
|
| 51 |
-
return None
|
| 52 |
-
return "\n".join(lines[start:end + 1])
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def flatten_to_oneliner(s: str) -> str:
|
| 56 |
-
if not s or "\n" not in s:
|
| 57 |
-
return s
|
| 58 |
-
return " ".join(s.split())
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# --- 1. plain assert -------------------------------------------------------
|
| 62 |
-
|
| 63 |
-
def _parse_plain_assert(source, node, lines):
|
| 64 |
-
src = get_source_segment(source, node)
|
| 65 |
-
if not src:
|
| 66 |
-
return None
|
| 67 |
-
lineno, col = node.lineno, node.col_offset
|
| 68 |
-
test = node.test
|
| 69 |
-
if isinstance(test, ast.Compare):
|
| 70 |
-
left_src = get_source_segment(source, test.left)
|
| 71 |
-
if not left_src:
|
| 72 |
-
return None
|
| 73 |
-
op = test.ops[0]
|
| 74 |
-
comparators = test.comparators
|
| 75 |
-
if not comparators:
|
| 76 |
-
return None
|
| 77 |
-
rhs_src = get_source_segment(source, comparators[0])
|
| 78 |
-
if not rhs_src:
|
| 79 |
-
return None
|
| 80 |
-
op_map = {
|
| 81 |
-
ast.Eq: " == ", ast.NotEq: " != ", ast.Lt: " < ", ast.LtE: " <= ",
|
| 82 |
-
ast.Gt: " > ", ast.GtE: " >= ", ast.In: " in ", ast.NotIn: " not in ",
|
| 83 |
-
ast.Is: " is ", ast.IsNot: " is not ",
|
| 84 |
-
}
|
| 85 |
-
op_str = op_map.get(type(op), " ??? ")
|
| 86 |
-
prefix = f"assert {left_src}{op_str}"
|
| 87 |
-
target = rhs_src
|
| 88 |
-
else:
|
| 89 |
-
prefix = "assert "
|
| 90 |
-
target = get_source_segment(source, test) or ""
|
| 91 |
-
return AssertionPair("assert", prefix, target, lineno, col)
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
# --- 2. self.assert* -------------------------------------------------------
|
| 95 |
-
|
| 96 |
-
def _parse_self_assert(source, node, lines):
|
| 97 |
-
if not isinstance(node.func, ast.Attribute):
|
| 98 |
-
return None
|
| 99 |
-
if not isinstance(node.func.value, ast.Name) or node.func.value.id != "self":
|
| 100 |
-
return None
|
| 101 |
-
method = node.func.attr
|
| 102 |
-
if method not in SELF_ASSERT_METHODS:
|
| 103 |
-
return None
|
| 104 |
-
args = node.args
|
| 105 |
-
if not args:
|
| 106 |
-
return None
|
| 107 |
-
if not get_source_segment(source, node):
|
| 108 |
-
return None
|
| 109 |
-
|
| 110 |
-
if method == "assertRaises":
|
| 111 |
-
exc_src = get_source_segment(source, args[0])
|
| 112 |
-
if exc_src:
|
| 113 |
-
return AssertionPair(f"self.{method}", "self.assertRaises(",
|
| 114 |
-
exc_src + ")", node.lineno, node.col_offset)
|
| 115 |
-
return None
|
| 116 |
-
if method == "assertRaisesRegex":
|
| 117 |
-
if len(args) >= 2:
|
| 118 |
-
exc_src = get_source_segment(source, args[0])
|
| 119 |
-
pat_src = get_source_segment(source, args[1])
|
| 120 |
-
if exc_src and pat_src:
|
| 121 |
-
return AssertionPair(f"self.{method}", "self.assertRaisesRegex(",
|
| 122 |
-
f"{exc_src}, {pat_src})",
|
| 123 |
-
node.lineno, node.col_offset)
|
| 124 |
-
return None
|
| 125 |
-
if method in ("assertTrue", "assertFalse", "assertIsNone", "assertIsNotNone"):
|
| 126 |
-
arg_src = get_source_segment(source, args[0])
|
| 127 |
-
if arg_src:
|
| 128 |
-
return AssertionPair(f"self.{method}", f"self.{method}(",
|
| 129 |
-
arg_src + ")", node.lineno, node.col_offset)
|
| 130 |
-
return None
|
| 131 |
-
if len(args) >= 2:
|
| 132 |
-
last_src = get_source_segment(source, args[-1])
|
| 133 |
-
if last_src:
|
| 134 |
-
prefix_parts = [f"self.{method}("]
|
| 135 |
-
for i, arg in enumerate(args[:-1]):
|
| 136 |
-
arg_src = get_source_segment(source, arg)
|
| 137 |
-
if arg_src:
|
| 138 |
-
prefix_parts.append(arg_src)
|
| 139 |
-
if i < len(args) - 2:
|
| 140 |
-
prefix_parts.append(", ")
|
| 141 |
-
prefix = "".join(prefix_parts) + ", "
|
| 142 |
-
return AssertionPair(f"self.{method}", prefix, last_src + ")",
|
| 143 |
-
node.lineno, node.col_offset)
|
| 144 |
-
return None
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
# --- 3. pytest.raises ------------------------------------------------------
|
| 148 |
-
|
| 149 |
-
def _parse_pytest_raises(source, node, lines):
|
| 150 |
-
if not isinstance(node.func, ast.Attribute):
|
| 151 |
-
return None
|
| 152 |
-
val = node.func.value
|
| 153 |
-
if not isinstance(val, ast.Name) or val.id != "pytest" or node.func.attr != "raises":
|
| 154 |
-
return None
|
| 155 |
-
args = node.args
|
| 156 |
-
if not args:
|
| 157 |
-
return None
|
| 158 |
-
exc_src = get_source_segment(source, args[0])
|
| 159 |
-
if not exc_src:
|
| 160 |
-
return None
|
| 161 |
-
kw_parts = []
|
| 162 |
-
for kw in node.keywords:
|
| 163 |
-
if kw.arg and kw.value:
|
| 164 |
-
kw_src = get_source_segment(source, kw.value)
|
| 165 |
-
if kw_src:
|
| 166 |
-
kw_parts.append(f', {kw.arg}={kw_src}')
|
| 167 |
-
target = exc_src + "".join(kw_parts) + ")"
|
| 168 |
-
return AssertionPair("pytest.raises", "pytest.raises(", target,
|
| 169 |
-
node.lineno, node.col_offset)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
# --- 4. pytest.approx ------------------------------------------------------
|
| 173 |
-
|
| 174 |
-
def _parse_pytest_approx(source, node, lines):
|
| 175 |
-
if not isinstance(node.func, ast.Attribute):
|
| 176 |
-
return None
|
| 177 |
-
val = node.func.value
|
| 178 |
-
if not isinstance(val, ast.Name) or val.id != "pytest" or node.func.attr != "approx":
|
| 179 |
-
return None
|
| 180 |
-
args = node.args
|
| 181 |
-
if not args:
|
| 182 |
-
return None
|
| 183 |
-
arg_src = get_source_segment(source, args[0])
|
| 184 |
-
if not arg_src:
|
| 185 |
-
return None
|
| 186 |
-
kw_parts = []
|
| 187 |
-
for kw in node.keywords:
|
| 188 |
-
if kw.arg and kw.value:
|
| 189 |
-
kw_src = get_source_segment(source, kw.value)
|
| 190 |
-
if kw_src:
|
| 191 |
-
kw_parts.append(f', {kw.arg}={kw_src}')
|
| 192 |
-
target = arg_src + "".join(kw_parts) + ")"
|
| 193 |
-
return AssertionPair("pytest.approx", "pytest.approx(", target,
|
| 194 |
-
node.lineno, node.col_offset)
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
# --- 5. assert_* (numpy/testing) ------------------------------------------
|
| 198 |
-
|
| 199 |
-
def _parse_assert_underscore(source, node, lines):
|
| 200 |
-
func = node.func
|
| 201 |
-
if isinstance(func, ast.Attribute):
|
| 202 |
-
name = func.attr
|
| 203 |
-
if not name.startswith("assert_") or name == "assert_":
|
| 204 |
-
return None
|
| 205 |
-
if isinstance(func.value, ast.Attribute):
|
| 206 |
-
if func.value.attr == "mock" or (
|
| 207 |
-
isinstance(func.value.value, ast.Name)
|
| 208 |
-
and func.value.value.id == "unittest"
|
| 209 |
-
):
|
| 210 |
-
return None
|
| 211 |
-
elif isinstance(func, ast.Name):
|
| 212 |
-
name = func.id
|
| 213 |
-
if not name.startswith("assert_") or name == "assert_":
|
| 214 |
-
return None
|
| 215 |
-
else:
|
| 216 |
-
return None
|
| 217 |
-
args = node.args
|
| 218 |
-
if not args:
|
| 219 |
-
return None
|
| 220 |
-
last_src = get_source_segment(source, args[-1])
|
| 221 |
-
if not last_src:
|
| 222 |
-
return None
|
| 223 |
-
prefix_parts = [f"{get_source_segment(source, func) or name}("]
|
| 224 |
-
for i, arg in enumerate(args[:-1]):
|
| 225 |
-
arg_src = get_source_segment(source, arg)
|
| 226 |
-
if arg_src:
|
| 227 |
-
prefix_parts.append(arg_src)
|
| 228 |
-
if i < len(args) - 2:
|
| 229 |
-
prefix_parts.append(", ")
|
| 230 |
-
prefix = "".join(prefix_parts) + ", "
|
| 231 |
-
return AssertionPair("assert_*", prefix, last_src + ")",
|
| 232 |
-
node.lineno, node.col_offset)
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
def _find_enclosing_test(tree, node):
|
| 236 |
-
node_start = getattr(node, "lineno", 0)
|
| 237 |
-
node_end = getattr(node, "end_lineno", node_start)
|
| 238 |
-
best = None
|
| 239 |
-
for n in ast.walk(tree):
|
| 240 |
-
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 241 |
-
if "test" in n.name.lower():
|
| 242 |
-
s, e = getattr(n, "lineno", 0), getattr(n, "end_lineno", 0)
|
| 243 |
-
if s <= node_start and node_end <= e:
|
| 244 |
-
if best is None or (getattr(best, "lineno", 0) < s):
|
| 245 |
-
best = n
|
| 246 |
-
if isinstance(n, ast.ClassDef) and "test" in n.name.lower():
|
| 247 |
-
s, e = getattr(n, "lineno", 0), getattr(n, "end_lineno", 0)
|
| 248 |
-
if s <= node_start and node_end <= e:
|
| 249 |
-
for item in n.body:
|
| 250 |
-
if (isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef))
|
| 251 |
-
and item.name.startswith("test_")):
|
| 252 |
-
is_, ie = getattr(item, "lineno", 0), getattr(item, "end_lineno", 0)
|
| 253 |
-
if is_ <= node_start and node_end <= ie:
|
| 254 |
-
if best is None or getattr(best, "lineno", 0) < is_:
|
| 255 |
-
best = item
|
| 256 |
-
return best
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
# ---------------------------------------------------------------------------
|
| 260 |
-
# Full-file-prefix extractor (build_commit_parquet_db.py)
|
| 261 |
-
# ---------------------------------------------------------------------------
|
| 262 |
-
|
| 263 |
-
@dataclass
|
| 264 |
-
class ExtractedAssertion:
|
| 265 |
-
assertion_type: str
|
| 266 |
-
prefix: str
|
| 267 |
-
target: str
|
| 268 |
-
lineno: int
|
| 269 |
-
col_offset: int
|
| 270 |
-
test_function: str
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
_WHITESPACE_RE = re.compile(r"\s+")
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
def normalize_for_id(s: str) -> str:
|
| 277 |
-
return _WHITESPACE_RE.sub(" ", (s or "").strip())
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
def _full_prefix_for(source_lines: List[str], pair: AssertionPair) -> str:
|
| 281 |
-
lineno = pair.lineno
|
| 282 |
-
col = pair.col_offset
|
| 283 |
-
if lineno < 1:
|
| 284 |
-
return pair.prefix
|
| 285 |
-
pre = source_lines[: lineno - 1]
|
| 286 |
-
last_line = source_lines[lineno - 1] if lineno - 1 < len(source_lines) else ""
|
| 287 |
-
if col < 0:
|
| 288 |
-
col = 0
|
| 289 |
-
last_line_prefix = last_line[:col] if col <= len(last_line) else last_line
|
| 290 |
-
return "".join(pre) + last_line_prefix + pair.prefix
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
def extract_assertions_from_source(source: str) -> List[ExtractedAssertion]:
|
| 294 |
-
"""Walk the AST and return full-file-prefix assertions for one file."""
|
| 295 |
-
if not source:
|
| 296 |
-
return []
|
| 297 |
-
try:
|
| 298 |
-
with warnings.catch_warnings():
|
| 299 |
-
warnings.simplefilter("ignore", SyntaxWarning)
|
| 300 |
-
tree = ast.parse(source)
|
| 301 |
-
except (SyntaxError, ValueError):
|
| 302 |
-
return []
|
| 303 |
-
|
| 304 |
-
lines = source.splitlines(keepends=True)
|
| 305 |
-
out: List[ExtractedAssertion] = []
|
| 306 |
-
|
| 307 |
-
for node in ast.walk(tree):
|
| 308 |
-
pair: Optional[AssertionPair] = None
|
| 309 |
-
if isinstance(node, ast.Assert):
|
| 310 |
-
pair = _parse_plain_assert(source, node, lines)
|
| 311 |
-
elif isinstance(node, ast.Call):
|
| 312 |
-
func = node.func
|
| 313 |
-
if isinstance(func, ast.Attribute):
|
| 314 |
-
val = func.value
|
| 315 |
-
val_id = getattr(val, "id", None) if isinstance(val, ast.Name) else None
|
| 316 |
-
if val_id == "self" and func.attr in SELF_ASSERT_METHODS:
|
| 317 |
-
pair = _parse_self_assert(source, node, lines)
|
| 318 |
-
elif val_id == "pytest":
|
| 319 |
-
if func.attr == "raises":
|
| 320 |
-
pair = _parse_pytest_raises(source, node, lines)
|
| 321 |
-
elif func.attr == "approx":
|
| 322 |
-
pair = _parse_pytest_approx(source, node, lines)
|
| 323 |
-
else:
|
| 324 |
-
pair = _parse_assert_underscore(source, node, lines)
|
| 325 |
-
else:
|
| 326 |
-
pair = _parse_assert_underscore(source, node, lines)
|
| 327 |
-
elif isinstance(func, ast.Name) and func.id.startswith("assert_"):
|
| 328 |
-
pair = _parse_assert_underscore(source, node, lines)
|
| 329 |
-
|
| 330 |
-
if pair is None or not pair.target.strip():
|
| 331 |
-
continue
|
| 332 |
-
target = pair.target
|
| 333 |
-
if target.lstrip().startswith(","):
|
| 334 |
-
continue
|
| 335 |
-
if "\n" in target:
|
| 336 |
-
target = flatten_to_oneliner(target)
|
| 337 |
-
if not target.strip():
|
| 338 |
-
continue
|
| 339 |
-
|
| 340 |
-
full_prefix = _full_prefix_for(lines, pair)
|
| 341 |
-
test_node = _find_enclosing_test(tree, node)
|
| 342 |
-
test_name = test_node.name if test_node else ""
|
| 343 |
-
out.append(ExtractedAssertion(
|
| 344 |
-
assertion_type=pair.assertion_type,
|
| 345 |
-
prefix=full_prefix,
|
| 346 |
-
target=target,
|
| 347 |
-
lineno=pair.lineno,
|
| 348 |
-
col_offset=pair.col_offset,
|
| 349 |
-
test_function=test_name,
|
| 350 |
-
))
|
| 351 |
-
return out
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
__all__ = [
|
| 355 |
-
"AssertionPair",
|
| 356 |
-
"ExtractedAssertion",
|
| 357 |
-
"extract_assertions_from_source",
|
| 358 |
-
"normalize_for_id",
|
| 359 |
-
"SELF_ASSERT_METHODS",
|
| 360 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c2l_demo/embedding.py
DELETED
|
@@ -1,165 +0,0 @@
|
|
| 1 |
-
"""Frozen Qwen3-Embedding-0.6B encoder.
|
| 2 |
-
|
| 3 |
-
Reproduces the two embedding recipes used to build the Code2LoRA-GRU v2
|
| 4 |
-
dataset, verbatim:
|
| 5 |
-
|
| 6 |
-
* **Diff embedding** (``build_diff_embeddings_shard.py`` / ``DiffEmbedder``):
|
| 7 |
-
chunk the diff text at 512 tokens (overlap 64), masked-mean each chunk, then
|
| 8 |
-
``concat(MaxPool, MeanPool)`` over chunks -> 2048-d (no L2 norm).
|
| 9 |
-
|
| 10 |
-
* **Repo-state embedding** (``build_repo_state_embeddings_shard.py``):
|
| 11 |
-
chunk each .py file at 2048 tokens (overlap 256), masked-mean each chunk,
|
| 12 |
-
average the chunk vectors per file -> 1024-d; then over all files
|
| 13 |
-
``concat(mean_files, max_files)`` -> 2048-d and L2-normalize.
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
from __future__ import annotations
|
| 17 |
-
|
| 18 |
-
from typing import List, Optional
|
| 19 |
-
|
| 20 |
-
import numpy as np
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
EMBED_MODEL_NAME = "Qwen/Qwen3-Embedding-0.6B"
|
| 24 |
-
|
| 25 |
-
# Diff recipe
|
| 26 |
-
DIFF_CHUNK_TOKENS = 512
|
| 27 |
-
DIFF_OVERLAP = 64
|
| 28 |
-
DIFF_MAX_LENGTH = 512
|
| 29 |
-
DIFF_MIN_WINDOW = 16
|
| 30 |
-
|
| 31 |
-
# Repo-state recipe
|
| 32 |
-
REPO_CHUNK_TOKENS = 2048
|
| 33 |
-
REPO_OVERLAP = 256
|
| 34 |
-
REPO_MIN_WINDOW = 8
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def _chunk_token_ids(token_ids: List[int], chunk_tokens: int, overlap: int,
|
| 38 |
-
min_window: int) -> List[List[int]]:
|
| 39 |
-
if chunk_tokens <= 0 or overlap >= chunk_tokens:
|
| 40 |
-
return []
|
| 41 |
-
chunks = []
|
| 42 |
-
step = chunk_tokens - overlap
|
| 43 |
-
n = len(token_ids)
|
| 44 |
-
for start in range(0, n, step):
|
| 45 |
-
end = min(start + chunk_tokens, n)
|
| 46 |
-
window = token_ids[start:end]
|
| 47 |
-
if len(window) < min_window:
|
| 48 |
-
if end >= n:
|
| 49 |
-
break
|
| 50 |
-
continue
|
| 51 |
-
chunks.append(window)
|
| 52 |
-
if end >= n:
|
| 53 |
-
break
|
| 54 |
-
return chunks
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
class Qwen3Embedder:
|
| 58 |
-
"""Lazy-loading wrapper around the frozen Qwen3-Embedding-0.6B encoder."""
|
| 59 |
-
|
| 60 |
-
def __init__(self, device: str = "cpu",
|
| 61 |
-
dtype: Optional[torch.dtype] = None,
|
| 62 |
-
batch_size: int = 8,
|
| 63 |
-
model_name: str = EMBED_MODEL_NAME):
|
| 64 |
-
self.device = device
|
| 65 |
-
self.dtype = dtype
|
| 66 |
-
self.batch_size = batch_size
|
| 67 |
-
self.model_name = model_name
|
| 68 |
-
self._model = None
|
| 69 |
-
self._tokenizer = None
|
| 70 |
-
self._hidden = None
|
| 71 |
-
|
| 72 |
-
def load(self) -> None:
|
| 73 |
-
if self._model is not None:
|
| 74 |
-
return
|
| 75 |
-
from transformers import AutoModel, AutoTokenizer
|
| 76 |
-
self._tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
| 77 |
-
model = AutoModel.from_pretrained(self.model_name)
|
| 78 |
-
if self.dtype is not None:
|
| 79 |
-
model = model.to(self.dtype)
|
| 80 |
-
self._model = model.to(self.device).eval()
|
| 81 |
-
self._hidden = int(self._model.config.hidden_size)
|
| 82 |
-
|
| 83 |
-
@property
|
| 84 |
-
def hidden_size(self) -> int:
|
| 85 |
-
self.load()
|
| 86 |
-
return self._hidden
|
| 87 |
-
|
| 88 |
-
@property
|
| 89 |
-
def embed_dim(self) -> int:
|
| 90 |
-
return 2 * self.hidden_size
|
| 91 |
-
|
| 92 |
-
@torch.no_grad()
|
| 93 |
-
def _embed_windows(self, texts: List[str], max_length: int) -> torch.Tensor:
|
| 94 |
-
"""[N, D] fp32 (cpu) masked-mean chunk embeddings."""
|
| 95 |
-
if not texts:
|
| 96 |
-
return torch.empty((0, self.hidden_size))
|
| 97 |
-
all_vecs = []
|
| 98 |
-
for i in range(0, len(texts), self.batch_size):
|
| 99 |
-
batch = texts[i:i + self.batch_size]
|
| 100 |
-
enc = self._tokenizer(batch, padding=True, truncation=True,
|
| 101 |
-
max_length=max_length, return_tensors="pt")
|
| 102 |
-
enc = {k: v.to(self.device) for k, v in enc.items()}
|
| 103 |
-
out = self._model(**enc)
|
| 104 |
-
last = out.last_hidden_state
|
| 105 |
-
mask = enc["attention_mask"].unsqueeze(-1).to(last.dtype)
|
| 106 |
-
mean = (last * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1)
|
| 107 |
-
all_vecs.append(mean.detach().float().cpu())
|
| 108 |
-
return torch.cat(all_vecs, dim=0)
|
| 109 |
-
|
| 110 |
-
# ---- Diff embedding ----
|
| 111 |
-
|
| 112 |
-
@torch.no_grad()
|
| 113 |
-
def embed_diff(self, diff_text: str) -> np.ndarray:
|
| 114 |
-
"""One diff -> [2*D] fp32 (MaxPool || MeanPool, no L2 norm)."""
|
| 115 |
-
self.load()
|
| 116 |
-
zero = np.zeros(2 * self._hidden, dtype=np.float32)
|
| 117 |
-
if not diff_text or not diff_text.strip():
|
| 118 |
-
return zero
|
| 119 |
-
ids = self._tokenizer.encode(diff_text, add_special_tokens=False)
|
| 120 |
-
windows = _chunk_token_ids(ids, DIFF_CHUNK_TOKENS, DIFF_OVERLAP, DIFF_MIN_WINDOW)
|
| 121 |
-
if not windows:
|
| 122 |
-
return zero
|
| 123 |
-
texts = [self._tokenizer.decode(w, skip_special_tokens=True) for w in windows]
|
| 124 |
-
chunk_embs = self._embed_windows(texts, DIFF_MAX_LENGTH) # [K, D]
|
| 125 |
-
mean_pool = chunk_embs.mean(dim=0)
|
| 126 |
-
max_pool = chunk_embs.max(dim=0).values
|
| 127 |
-
return torch.cat([max_pool, mean_pool], dim=-1).numpy().astype(np.float32)
|
| 128 |
-
|
| 129 |
-
# ---- Repo-state embedding ----
|
| 130 |
-
|
| 131 |
-
@torch.no_grad()
|
| 132 |
-
def _embed_file(self, text: str) -> Optional[np.ndarray]:
|
| 133 |
-
ids = self._tokenizer.encode(text, add_special_tokens=False)
|
| 134 |
-
if not ids:
|
| 135 |
-
return None
|
| 136 |
-
windows = _chunk_token_ids(ids, REPO_CHUNK_TOKENS, REPO_OVERLAP, REPO_MIN_WINDOW)
|
| 137 |
-
if not windows:
|
| 138 |
-
return None
|
| 139 |
-
texts = [self._tokenizer.decode(w, skip_special_tokens=True) for w in windows]
|
| 140 |
-
chunk_embs = self._embed_windows(texts, REPO_CHUNK_TOKENS) # [K, D]
|
| 141 |
-
return chunk_embs.mean(dim=0).numpy().astype(np.float32)
|
| 142 |
-
|
| 143 |
-
@torch.no_grad()
|
| 144 |
-
def embed_repo_state(self, file_texts: List[str]) -> np.ndarray:
|
| 145 |
-
"""List of file contents -> [2*D] L2-normalized repo-state vector."""
|
| 146 |
-
self.load()
|
| 147 |
-
repo_dim = 2 * self._hidden
|
| 148 |
-
file_vecs = []
|
| 149 |
-
for text in file_texts:
|
| 150 |
-
if not text:
|
| 151 |
-
continue
|
| 152 |
-
vec = self._embed_file(text)
|
| 153 |
-
if vec is not None:
|
| 154 |
-
file_vecs.append(vec)
|
| 155 |
-
if not file_vecs:
|
| 156 |
-
return np.zeros(repo_dim, dtype=np.float32)
|
| 157 |
-
sub = np.stack(file_vecs).astype(np.float32)
|
| 158 |
-
mean_pool = sub.mean(axis=0)
|
| 159 |
-
max_pool = sub.max(axis=0)
|
| 160 |
-
repo_vec = np.concatenate([mean_pool, max_pool], axis=0)
|
| 161 |
-
norm = float(np.linalg.norm(repo_vec)) + 1e-12
|
| 162 |
-
return (repo_vec / norm).astype(np.float32)
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
__all__ = ["Qwen3Embedder", "EMBED_MODEL_NAME"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c2l_demo/engine.py
DELETED
|
@@ -1,400 +0,0 @@
|
|
| 1 |
-
"""End-to-end inference engine for the Code2LoRA-GRU demo.
|
| 2 |
-
|
| 3 |
-
Ties everything together:
|
| 4 |
-
|
| 5 |
-
1. resolve a repository (local path or git URL -> shallow-free clone),
|
| 6 |
-
2. select the kept "GRU-pass" commits + per-repo statistics,
|
| 7 |
-
3. embed each commit's production-code diff and the initial repo state with
|
| 8 |
-
the frozen Qwen3-Embedding encoder,
|
| 9 |
-
4. walk the commits through the :class:`CommitGRU`, recording the hidden-state
|
| 10 |
-
trajectory,
|
| 11 |
-
5. generate a repository-conditioned LoRA from the hidden state and inject it
|
| 12 |
-
into the frozen Qwen2.5-Coder base model,
|
| 13 |
-
6. predict a held-out assertion both with and without the adapter.
|
| 14 |
-
|
| 15 |
-
Heavy models are loaded lazily and cached on the process-wide
|
| 16 |
-
:class:`Engine`. Per-repository state lives in a :class:`RepoSession`.
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import os
|
| 22 |
-
import re
|
| 23 |
-
import shutil
|
| 24 |
-
import subprocess
|
| 25 |
-
import tempfile
|
| 26 |
-
import time
|
| 27 |
-
from dataclasses import dataclass, field
|
| 28 |
-
from pathlib import Path
|
| 29 |
-
from typing import Callable, Dict, List, Optional, Tuple
|
| 30 |
-
|
| 31 |
-
import numpy as np
|
| 32 |
-
import torch
|
| 33 |
-
|
| 34 |
-
from .core import (
|
| 35 |
-
Code2LoRAHead,
|
| 36 |
-
CommitGRU,
|
| 37 |
-
clear_all_lora_weights,
|
| 38 |
-
discover_module_types_and_dims,
|
| 39 |
-
get_module_specs,
|
| 40 |
-
inject_lora_weights,
|
| 41 |
-
replace_with_lora,
|
| 42 |
-
)
|
| 43 |
-
from .embedding import Qwen3Embedder
|
| 44 |
-
from .git_pipeline import RepoProcessResult, ls_tree_py, process_repo, GitCatFileBatch
|
| 45 |
-
from .metrics import compute_metrics
|
| 46 |
-
|
| 47 |
-
DEFAULT_BASE_MODEL = "Qwen/Qwen2.5-Coder-1.5B"
|
| 48 |
-
DEFAULT_TARGET_MODULES = [
|
| 49 |
-
"q_proj", "k_proj", "v_proj", "o_proj",
|
| 50 |
-
"up_proj", "gate_proj", "down_proj",
|
| 51 |
-
]
|
| 52 |
-
|
| 53 |
-
# Where to find the trained GRU+head checkpoint. Either a local path
|
| 54 |
-
# (CODE2LORA_CKPT) or a HF Hub model repo (CODE2LORA_CKPT_REPO + _FILE).
|
| 55 |
-
ENV_CKPT_PATH = "CODE2LORA_CKPT"
|
| 56 |
-
ENV_CKPT_REPO = "CODE2LORA_CKPT_REPO"
|
| 57 |
-
ENV_CKPT_FILE = "CODE2LORA_CKPT_FILE"
|
| 58 |
-
# The published paper checkpoint (GRU + Code2LoRAHead). Used by default so the
|
| 59 |
-
# Space works zero-config; override with the env vars above if needed.
|
| 60 |
-
DEFAULT_CKPT_REPO = "code2lora/code2lora-gru"
|
| 61 |
-
DEFAULT_CKPT_FILE = "code2lora_gru.pt"
|
| 62 |
-
|
| 63 |
-
ProgressFn = Optional[Callable[[str, float], None]]
|
| 64 |
-
|
| 65 |
-
_GIT_URL_RE = re.compile(r"^(https?://|git@|ssh://)")
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def _is_git_url(s: str) -> bool:
|
| 69 |
-
return bool(_GIT_URL_RE.match(s.strip()))
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
# ---------------------------------------------------------------------------
|
| 73 |
-
# Per-repository session
|
| 74 |
-
# ---------------------------------------------------------------------------
|
| 75 |
-
|
| 76 |
-
@dataclass
|
| 77 |
-
class RepoSession:
|
| 78 |
-
repo_id: str
|
| 79 |
-
result: RepoProcessResult
|
| 80 |
-
# GRU trajectory over the walked commits:
|
| 81 |
-
walked_indices: List[int] # kept-commit indices walked
|
| 82 |
-
hidden_states: np.ndarray # [T, H] post-step h[-1]
|
| 83 |
-
ctx_norms: List[float] # ||output_norm(h_t)||
|
| 84 |
-
endpoint_index: int # kept-commit index of the adapter
|
| 85 |
-
repo_state_norm: float
|
| 86 |
-
n_repo_state_files: int
|
| 87 |
-
# cached tensors for re-prediction without re-walking:
|
| 88 |
-
_hidden_endpoint: Optional[torch.Tensor] = None # [H] on engine device
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
# ---------------------------------------------------------------------------
|
| 92 |
-
# Engine
|
| 93 |
-
# ---------------------------------------------------------------------------
|
| 94 |
-
|
| 95 |
-
class Engine:
|
| 96 |
-
def __init__(self,
|
| 97 |
-
base_model_name: str = DEFAULT_BASE_MODEL,
|
| 98 |
-
target_modules: Optional[List[str]] = None,
|
| 99 |
-
device: Optional[str] = None,
|
| 100 |
-
max_repo_state_files: int = 400):
|
| 101 |
-
self.base_model_name = base_model_name
|
| 102 |
-
self.target_modules = target_modules or list(DEFAULT_TARGET_MODULES)
|
| 103 |
-
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 104 |
-
self.dtype = torch.bfloat16 if self.device == "cuda" else torch.float32
|
| 105 |
-
self.max_repo_state_files = max_repo_state_files
|
| 106 |
-
|
| 107 |
-
self._tokenizer = None
|
| 108 |
-
self._base_model = None
|
| 109 |
-
self._specs = None
|
| 110 |
-
self._gru: Optional[CommitGRU] = None
|
| 111 |
-
self._head: Optional[Code2LoRAHead] = None
|
| 112 |
-
self._embedder: Optional[Qwen3Embedder] = None
|
| 113 |
-
self._loaded = False
|
| 114 |
-
|
| 115 |
-
# ---- lazy loading ----
|
| 116 |
-
|
| 117 |
-
@property
|
| 118 |
-
def embedder(self) -> Qwen3Embedder:
|
| 119 |
-
if self._embedder is None:
|
| 120 |
-
self._embedder = Qwen3Embedder(
|
| 121 |
-
device=self.device,
|
| 122 |
-
dtype=(torch.float16 if self.device == "cuda" else None),
|
| 123 |
-
)
|
| 124 |
-
return self._embedder
|
| 125 |
-
|
| 126 |
-
def _resolve_ckpt(self) -> str:
|
| 127 |
-
local = os.environ.get(ENV_CKPT_PATH)
|
| 128 |
-
if local:
|
| 129 |
-
if Path(local).exists():
|
| 130 |
-
return local
|
| 131 |
-
raise FileNotFoundError(f"{ENV_CKPT_PATH}={local} does not exist.")
|
| 132 |
-
from huggingface_hub import hf_hub_download
|
| 133 |
-
repo = os.environ.get(ENV_CKPT_REPO, DEFAULT_CKPT_REPO)
|
| 134 |
-
fname = os.environ.get(ENV_CKPT_FILE, DEFAULT_CKPT_FILE)
|
| 135 |
-
return hf_hub_download(repo_id=repo, filename=fname)
|
| 136 |
-
|
| 137 |
-
def load(self, progress: ProgressFn = None) -> None:
|
| 138 |
-
if self._loaded:
|
| 139 |
-
return
|
| 140 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 141 |
-
|
| 142 |
-
def _p(msg, frac):
|
| 143 |
-
if progress:
|
| 144 |
-
progress(msg, frac)
|
| 145 |
-
|
| 146 |
-
_p("Loading checkpoint…", 0.05)
|
| 147 |
-
ckpt_path = self._resolve_ckpt()
|
| 148 |
-
state = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
| 149 |
-
head_cfg = state.get("head_config") or {}
|
| 150 |
-
gru_cfg = state.get("gru_config") or {}
|
| 151 |
-
rank = int(head_cfg.get("rank", 16))
|
| 152 |
-
alpha = float((state.get("args") or {}).get("alpha", 32.0))
|
| 153 |
-
head_hidden = int(head_cfg.get("hidden_dim", 1024))
|
| 154 |
-
|
| 155 |
-
_p(f"Loading base model {self.base_model_name}…", 0.2)
|
| 156 |
-
self._tokenizer = AutoTokenizer.from_pretrained(self.base_model_name)
|
| 157 |
-
if self._tokenizer.pad_token is None:
|
| 158 |
-
self._tokenizer.pad_token = self._tokenizer.eos_token
|
| 159 |
-
self._base_model = self._load_base_model()
|
| 160 |
-
self._base_model.eval()
|
| 161 |
-
for p in self._base_model.parameters():
|
| 162 |
-
p.requires_grad = False
|
| 163 |
-
|
| 164 |
-
_p("Wiring LoRA modules…", 0.7)
|
| 165 |
-
self._specs = get_module_specs(self._base_model, self.target_modules)
|
| 166 |
-
type_dims = discover_module_types_and_dims(self._specs)
|
| 167 |
-
input_dim = int(head_cfg.get("input_dim", gru_cfg.get("hidden_dim", 2048)))
|
| 168 |
-
|
| 169 |
-
self._head = Code2LoRAHead(input_dim=input_dim, type_dims=type_dims,
|
| 170 |
-
hidden_dim=head_hidden, rank=rank).to(self.device)
|
| 171 |
-
self._head.load_state_dict(state["head_state"])
|
| 172 |
-
self._head.eval()
|
| 173 |
-
|
| 174 |
-
self._gru = CommitGRU(
|
| 175 |
-
diff_input_dim=int(gru_cfg.get("diff_input_dim", 2048)),
|
| 176 |
-
repo_state_dim=int(gru_cfg.get("repo_state_dim", 2048)),
|
| 177 |
-
hidden_dim=int(gru_cfg.get("hidden_dim", 2048)),
|
| 178 |
-
).to(self.device)
|
| 179 |
-
self._gru.load_state_dict(state["gru_state"])
|
| 180 |
-
self._gru.eval()
|
| 181 |
-
|
| 182 |
-
replace_with_lora(self._base_model, self._specs, rank=rank, alpha=alpha)
|
| 183 |
-
_p("Models ready.", 1.0)
|
| 184 |
-
self._loaded = True
|
| 185 |
-
|
| 186 |
-
def _load_base_model(self):
|
| 187 |
-
from transformers import AutoModelForCausalLM
|
| 188 |
-
last_err = None
|
| 189 |
-
for attn in ("flash_attention_2", "sdpa", "eager"):
|
| 190 |
-
try:
|
| 191 |
-
return AutoModelForCausalLM.from_pretrained(
|
| 192 |
-
self.base_model_name,
|
| 193 |
-
torch_dtype=self.dtype,
|
| 194 |
-
attn_implementation=attn,
|
| 195 |
-
device_map={"": self.device} if self.device == "cuda" else None,
|
| 196 |
-
).to(self.device)
|
| 197 |
-
except Exception as e: # flash-attn may be unavailable
|
| 198 |
-
last_err = e
|
| 199 |
-
continue
|
| 200 |
-
raise RuntimeError(f"Could not load base model: {last_err}")
|
| 201 |
-
|
| 202 |
-
# ---- repository resolution ----
|
| 203 |
-
|
| 204 |
-
def resolve_repo(self, repo_input: str, work_dir: Path,
|
| 205 |
-
progress: ProgressFn = None) -> Tuple[Path, str]:
|
| 206 |
-
"""Return (local_repo_dir, repo_id). Clones if a URL is given."""
|
| 207 |
-
repo_input = repo_input.strip()
|
| 208 |
-
if not repo_input:
|
| 209 |
-
raise ValueError("Please provide a repository path or git URL.")
|
| 210 |
-
if _is_git_url(repo_input):
|
| 211 |
-
repo_id = _repo_id_from_url(repo_input)
|
| 212 |
-
dest = work_dir / repo_id.replace("/", "__")
|
| 213 |
-
if dest.exists():
|
| 214 |
-
shutil.rmtree(dest, ignore_errors=True)
|
| 215 |
-
if progress:
|
| 216 |
-
progress(f"Cloning {repo_input}…", 0.0)
|
| 217 |
-
res = subprocess.run(
|
| 218 |
-
["git", "clone", "--no-single-branch", repo_input, str(dest)],
|
| 219 |
-
capture_output=True, text=True, timeout=900)
|
| 220 |
-
if res.returncode != 0 or not (dest / ".git").exists():
|
| 221 |
-
raise RuntimeError(f"git clone failed:\n{res.stderr[-2000:]}")
|
| 222 |
-
return dest, repo_id
|
| 223 |
-
path = Path(repo_input).expanduser()
|
| 224 |
-
if not (path / ".git").exists():
|
| 225 |
-
raise ValueError(f"{path} is not a git repository (no .git found).")
|
| 226 |
-
return path, _repo_id_from_path(path)
|
| 227 |
-
|
| 228 |
-
# ---- processing + GRU walk ----
|
| 229 |
-
|
| 230 |
-
def process_and_walk(self, repo_dir: Path, repo_id: str,
|
| 231 |
-
walk_fraction: float = 0.8,
|
| 232 |
-
progress: ProgressFn = None) -> RepoSession:
|
| 233 |
-
"""Full pipeline: select commits, embed, walk the GRU, cache the
|
| 234 |
-
adapter context at the requested walk endpoint."""
|
| 235 |
-
self.load(progress=progress)
|
| 236 |
-
|
| 237 |
-
def _p(msg, frac):
|
| 238 |
-
if progress:
|
| 239 |
-
progress(msg, frac)
|
| 240 |
-
|
| 241 |
-
result = process_repo(repo_dir, repo_id=repo_id, progress=progress)
|
| 242 |
-
if result.kept_commits == 0:
|
| 243 |
-
raise RuntimeError(
|
| 244 |
-
"No commits introduced new assertions in test files — this "
|
| 245 |
-
"repository has no usable Code2LoRA history. Pick a repo with "
|
| 246 |
-
"a pytest/unittest suite that grew over time.")
|
| 247 |
-
|
| 248 |
-
# Endpoint: the last train-split commit, i.e. the chronological 80%
|
| 249 |
-
# boundary defined by the dataset's 80/10/10 split. The val/test tail
|
| 250 |
-
# is never walked, so predictions on it are genuine generalization.
|
| 251 |
-
n_kept = result.kept_commits
|
| 252 |
-
endpoint = result.split_boundary_index
|
| 253 |
-
if endpoint < 0:
|
| 254 |
-
endpoint = max(0, min(n_kept - 1, int(round(walk_fraction * n_kept)) - 1))
|
| 255 |
-
|
| 256 |
-
# ---- repo-state embedding @ commit 0 ----
|
| 257 |
-
_p("Embedding initial repository state…", 0.0)
|
| 258 |
-
repo_state_0, n_files = self._embed_repo_state(repo_dir, result.commits[0].sha)
|
| 259 |
-
|
| 260 |
-
# ---- walk the GRU, embedding diffs on the way ----
|
| 261 |
-
gru = self._gru
|
| 262 |
-
device = self.device
|
| 263 |
-
h = gru.init_hidden(torch.from_numpy(repo_state_0).to(device).unsqueeze(0))
|
| 264 |
-
|
| 265 |
-
hidden_states: List[np.ndarray] = []
|
| 266 |
-
ctx_norms: List[float] = []
|
| 267 |
-
walked_indices: List[int] = []
|
| 268 |
-
hidden_endpoint: Optional[torch.Tensor] = None
|
| 269 |
-
|
| 270 |
-
with torch.no_grad():
|
| 271 |
-
for ci in range(endpoint + 1):
|
| 272 |
-
commit = result.commits[ci]
|
| 273 |
-
_p(f"GRU streaming commit {ci + 1}/{endpoint + 1}…",
|
| 274 |
-
(ci + 1) / max(endpoint + 1, 1))
|
| 275 |
-
diff_emb = self.embedder.embed_diff(commit.production_code_diff)
|
| 276 |
-
diff_t = torch.from_numpy(diff_emb).to(device).unsqueeze(0)
|
| 277 |
-
h = gru.step(diff_t, h)
|
| 278 |
-
h_last = h[-1].squeeze(0) # [H]
|
| 279 |
-
ctx = gru.output_norm(h[-1]) # [1, H]
|
| 280 |
-
hidden_states.append(h_last.float().cpu().numpy())
|
| 281 |
-
ctx_norms.append(float(ctx.float().norm().cpu()))
|
| 282 |
-
walked_indices.append(ci)
|
| 283 |
-
if ci == endpoint:
|
| 284 |
-
hidden_endpoint = h_last.detach()
|
| 285 |
-
|
| 286 |
-
return RepoSession(
|
| 287 |
-
repo_id=repo_id, result=result, walked_indices=walked_indices,
|
| 288 |
-
hidden_states=np.stack(hidden_states) if hidden_states else np.zeros((0, gru.hidden_dim)),
|
| 289 |
-
ctx_norms=ctx_norms, endpoint_index=endpoint,
|
| 290 |
-
repo_state_norm=float(np.linalg.norm(repo_state_0)),
|
| 291 |
-
n_repo_state_files=n_files, _hidden_endpoint=hidden_endpoint)
|
| 292 |
-
|
| 293 |
-
def _embed_repo_state(self, repo_dir: Path, commit_sha: str) -> Tuple[np.ndarray, int]:
|
| 294 |
-
files = ls_tree_py(repo_dir, commit_sha)
|
| 295 |
-
if len(files) > self.max_repo_state_files:
|
| 296 |
-
# Keep the largest files (most signal) under the cap.
|
| 297 |
-
files = sorted(files, key=lambda x: -x[2])[: self.max_repo_state_files]
|
| 298 |
-
texts: List[str] = []
|
| 299 |
-
with GitCatFileBatch(repo_dir) as cat:
|
| 300 |
-
for blob, _path, _size in files:
|
| 301 |
-
txt = cat.read_blob_text(blob)
|
| 302 |
-
if txt:
|
| 303 |
-
texts.append(txt)
|
| 304 |
-
vec = self.embedder.embed_repo_state(texts)
|
| 305 |
-
return vec, len(texts)
|
| 306 |
-
|
| 307 |
-
# ---- adapter generation ----
|
| 308 |
-
|
| 309 |
-
def _ctx_at(self, session: RepoSession, commit_index: Optional[int]) -> torch.Tensor:
|
| 310 |
-
"""output_norm(h_t) for the requested commit (defaults to endpoint)."""
|
| 311 |
-
if commit_index is None or commit_index == session.endpoint_index:
|
| 312 |
-
h_last = session._hidden_endpoint
|
| 313 |
-
else:
|
| 314 |
-
idx = max(0, min(commit_index, len(session.hidden_states) - 1))
|
| 315 |
-
h_last = torch.from_numpy(session.hidden_states[idx]).to(self.device)
|
| 316 |
-
h_last = h_last.unsqueeze(0) # [1, H]
|
| 317 |
-
return self._gru.output_norm(h_last) # [1, H]
|
| 318 |
-
|
| 319 |
-
# ---- prediction ----
|
| 320 |
-
|
| 321 |
-
@torch.no_grad()
|
| 322 |
-
def predict(self, session: RepoSession, prefix: str, target: str,
|
| 323 |
-
commit_index: Optional[int] = None,
|
| 324 |
-
max_input_tokens: int = 4096,
|
| 325 |
-
max_new_tokens: int = 32) -> Dict:
|
| 326 |
-
"""Predict the assertion RHS with the base model and with the adapter."""
|
| 327 |
-
tok = self._tokenizer
|
| 328 |
-
bos = getattr(tok, "bos_token_id", None)
|
| 329 |
-
if bos is None:
|
| 330 |
-
bos = getattr(tok, "eos_token_id", None)
|
| 331 |
-
ids = tok.encode(prefix, add_special_tokens=False)
|
| 332 |
-
if bos is not None:
|
| 333 |
-
ids = [bos] + ids
|
| 334 |
-
if len(ids) > max_input_tokens:
|
| 335 |
-
ids = ids[-max_input_tokens:]
|
| 336 |
-
|
| 337 |
-
# 1) base model (no adapter)
|
| 338 |
-
clear_all_lora_weights(self._base_model, self._specs)
|
| 339 |
-
base_pred = self._generate(ids, max_new_tokens)
|
| 340 |
-
|
| 341 |
-
# 2) adapted model
|
| 342 |
-
ctx = self._ctx_at(session, commit_index)
|
| 343 |
-
head_out = self._head(ctx)
|
| 344 |
-
inject_lora_weights(self._base_model, self._specs, head_out, batch_index=0)
|
| 345 |
-
adapted_pred = self._generate(ids, max_new_tokens)
|
| 346 |
-
clear_all_lora_weights(self._base_model, self._specs)
|
| 347 |
-
|
| 348 |
-
base_m = compute_metrics(base_pred, target)
|
| 349 |
-
adapted_m = compute_metrics(adapted_pred, target)
|
| 350 |
-
return {
|
| 351 |
-
"prefix": prefix,
|
| 352 |
-
"target": target,
|
| 353 |
-
"base_prediction": base_pred,
|
| 354 |
-
"adapted_prediction": adapted_pred,
|
| 355 |
-
"base_clean": base_m["pred_clean"],
|
| 356 |
-
"adapted_clean": adapted_m["pred_clean"],
|
| 357 |
-
"base_exact_match": bool(base_m["exact_match"]),
|
| 358 |
-
"adapted_exact_match": bool(adapted_m["exact_match"]),
|
| 359 |
-
"base_edit_similarity": float(base_m["edit_similarity"]),
|
| 360 |
-
"adapted_edit_similarity": float(adapted_m["edit_similarity"]),
|
| 361 |
-
"n_prompt_tokens": len(ids),
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
@torch.no_grad()
|
| 365 |
-
def _generate(self, input_ids: List[int], max_new_tokens: int) -> str:
|
| 366 |
-
device = self.device
|
| 367 |
-
ids = torch.tensor([input_ids], dtype=torch.long, device=device)
|
| 368 |
-
attn = torch.ones_like(ids)
|
| 369 |
-
out = self._base_model.generate(
|
| 370 |
-
input_ids=ids, attention_mask=attn,
|
| 371 |
-
max_new_tokens=int(max_new_tokens), do_sample=False,
|
| 372 |
-
pad_token_id=self._tokenizer.pad_token_id,
|
| 373 |
-
eos_token_id=self._tokenizer.eos_token_id, use_cache=True)
|
| 374 |
-
gen = out[0, ids.shape[1]:].tolist()
|
| 375 |
-
return self._tokenizer.decode(gen, skip_special_tokens=True)
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
# ---------------------------------------------------------------------------
|
| 379 |
-
# helpers
|
| 380 |
-
# ---------------------------------------------------------------------------
|
| 381 |
-
|
| 382 |
-
def _repo_id_from_url(url: str) -> str:
|
| 383 |
-
s = url.strip()
|
| 384 |
-
s = re.sub(r"\.git$", "", s)
|
| 385 |
-
s = re.sub(r"^git@[^:]+:", "", s)
|
| 386 |
-
s = re.sub(r"^(https?|ssh)://[^/]+/", "", s)
|
| 387 |
-
parts = [p for p in s.split("/") if p]
|
| 388 |
-
if len(parts) >= 2:
|
| 389 |
-
return f"{parts[-2]}/{parts[-1]}"
|
| 390 |
-
return parts[-1] if parts else "repo"
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
def _repo_id_from_path(path: Path) -> str:
|
| 394 |
-
parts = path.resolve().parts
|
| 395 |
-
if len(parts) >= 2:
|
| 396 |
-
return f"{parts[-2]}/{parts[-1]}"
|
| 397 |
-
return path.name
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
__all__ = ["Engine", "RepoSession", "DEFAULT_BASE_MODEL"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c2l_demo/git_pipeline.py
DELETED
|
@@ -1,498 +0,0 @@
|
|
| 1 |
-
"""Git → commit-sequence pipeline.
|
| 2 |
-
|
| 3 |
-
Faithful port of the repository-processing logic from
|
| 4 |
-
``create_dataset/build_commit_assertion_db.py`` and
|
| 5 |
-
``create_dataset/build_commit_parquet_db.py``:
|
| 6 |
-
|
| 7 |
-
* stream first-parent commits oldest-first,
|
| 8 |
-
* keep only commits that introduce *new or changed* assertions in test files
|
| 9 |
-
(this is the "GRU pass" subset),
|
| 10 |
-
* build the ``production_code_diff`` (non-test source) between consecutive
|
| 11 |
-
kept commits,
|
| 12 |
-
* split the kept commits chronologically 80/10/10 into train/val/test,
|
| 13 |
-
* emit one QnA per new assertion.
|
| 14 |
-
|
| 15 |
-
Everything runs against a local clone with full history.
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import re
|
| 21 |
-
import subprocess
|
| 22 |
-
from dataclasses import dataclass, field
|
| 23 |
-
from pathlib import Path
|
| 24 |
-
from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple
|
| 25 |
-
|
| 26 |
-
from .assertions import (
|
| 27 |
-
ExtractedAssertion,
|
| 28 |
-
extract_assertions_from_source,
|
| 29 |
-
normalize_for_id,
|
| 30 |
-
)
|
| 31 |
-
|
| 32 |
-
# ---------------------------------------------------------------------------
|
| 33 |
-
# Constants (must match the dataset builders)
|
| 34 |
-
# ---------------------------------------------------------------------------
|
| 35 |
-
|
| 36 |
-
SOURCE_EXTS = {".py", ".md", ".rst"}
|
| 37 |
-
SKIP_DIRS = {
|
| 38 |
-
".git", "__pycache__", ".venv", "venv", "env",
|
| 39 |
-
"node_modules", "dist", "build", ".tox", ".mypy_cache",
|
| 40 |
-
"TEST_HYPERNET",
|
| 41 |
-
}
|
| 42 |
-
EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
| 43 |
-
_NULL_SHA = "0000000000000000000000000000000000000000"
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
# ---------------------------------------------------------------------------
|
| 47 |
-
# Path filtering
|
| 48 |
-
# ---------------------------------------------------------------------------
|
| 49 |
-
|
| 50 |
-
def _is_test_path(path: str) -> bool:
|
| 51 |
-
for part in Path(path).parts:
|
| 52 |
-
if "test" in part.lower():
|
| 53 |
-
return True
|
| 54 |
-
return False
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def _is_python_test_file(path: str) -> bool:
|
| 58 |
-
p = Path(path)
|
| 59 |
-
if p.suffix.lower() != ".py":
|
| 60 |
-
return False
|
| 61 |
-
if any(part in SKIP_DIRS for part in p.parts):
|
| 62 |
-
return False
|
| 63 |
-
return _is_test_path(path)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def _should_skip_diff_path(path: str) -> bool:
|
| 67 |
-
p = Path(path)
|
| 68 |
-
if p.suffix.lower() not in SOURCE_EXTS:
|
| 69 |
-
return True
|
| 70 |
-
if any(part in SKIP_DIRS for part in p.parts):
|
| 71 |
-
return True
|
| 72 |
-
if _is_test_path(path):
|
| 73 |
-
return True
|
| 74 |
-
return False
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
# ---------------------------------------------------------------------------
|
| 78 |
-
# Git helpers
|
| 79 |
-
# ---------------------------------------------------------------------------
|
| 80 |
-
|
| 81 |
-
def _run_git(repo_dir: Path, args: List[str], timeout: int = 300) -> Optional[str]:
|
| 82 |
-
try:
|
| 83 |
-
result = subprocess.run(
|
| 84 |
-
["git", "-C", str(repo_dir)] + args,
|
| 85 |
-
capture_output=True, timeout=timeout,
|
| 86 |
-
)
|
| 87 |
-
if result.returncode != 0:
|
| 88 |
-
return None
|
| 89 |
-
return result.stdout.decode("utf-8", errors="replace")
|
| 90 |
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
| 91 |
-
return None
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
@dataclass
|
| 95 |
-
class RawFileChange:
|
| 96 |
-
status: str
|
| 97 |
-
new_blob: str
|
| 98 |
-
path: str
|
| 99 |
-
old_path: Optional[str] = None
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
@dataclass
|
| 103 |
-
class RawCommit:
|
| 104 |
-
sha: str
|
| 105 |
-
parent_sha: Optional[str]
|
| 106 |
-
timestamp: str
|
| 107 |
-
changes: List[RawFileChange]
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def stream_commit_changes(repo_dir: Path) -> Iterator[RawCommit]:
|
| 111 |
-
"""Yield one ``RawCommit`` per first-parent commit, oldest-first."""
|
| 112 |
-
proc = subprocess.Popen(
|
| 113 |
-
[
|
| 114 |
-
"git", "-C", str(repo_dir),
|
| 115 |
-
"log", "--first-parent", "--reverse",
|
| 116 |
-
"--raw", "--no-abbrev", "-M",
|
| 117 |
-
"--format=COMMITHDR %H %P %aI",
|
| 118 |
-
],
|
| 119 |
-
stdout=subprocess.PIPE,
|
| 120 |
-
stderr=subprocess.PIPE,
|
| 121 |
-
bufsize=1 << 20,
|
| 122 |
-
)
|
| 123 |
-
assert proc.stdout is not None
|
| 124 |
-
cur: Optional[RawCommit] = None
|
| 125 |
-
try:
|
| 126 |
-
for raw in proc.stdout:
|
| 127 |
-
line = raw.decode("utf-8", errors="replace").rstrip("\n")
|
| 128 |
-
if not line:
|
| 129 |
-
continue
|
| 130 |
-
if line.startswith("COMMITHDR "):
|
| 131 |
-
if cur is not None:
|
| 132 |
-
yield cur
|
| 133 |
-
tokens = line.split()
|
| 134 |
-
if len(tokens) < 3:
|
| 135 |
-
cur = None
|
| 136 |
-
continue
|
| 137 |
-
sha = tokens[1]
|
| 138 |
-
timestamp = tokens[-1]
|
| 139 |
-
parents = tokens[2:-1]
|
| 140 |
-
parent_sha = parents[0] if parents else None
|
| 141 |
-
cur = RawCommit(sha=sha, parent_sha=parent_sha,
|
| 142 |
-
timestamp=timestamp, changes=[])
|
| 143 |
-
continue
|
| 144 |
-
if cur is None or not line.startswith(":"):
|
| 145 |
-
continue
|
| 146 |
-
try:
|
| 147 |
-
meta, _, tail = line.partition("\t")
|
| 148 |
-
if not tail:
|
| 149 |
-
continue
|
| 150 |
-
fields = meta.split()
|
| 151 |
-
if len(fields) < 5:
|
| 152 |
-
continue
|
| 153 |
-
new_blob = fields[3].lstrip(":")
|
| 154 |
-
status = fields[4]
|
| 155 |
-
parts = tail.split("\t")
|
| 156 |
-
if status.startswith("R") or status.startswith("C"):
|
| 157 |
-
old_path = parts[0]
|
| 158 |
-
new_path = parts[1] if len(parts) > 1 else parts[0]
|
| 159 |
-
cur.changes.append(RawFileChange(
|
| 160 |
-
status=status, new_blob=new_blob,
|
| 161 |
-
path=new_path, old_path=old_path))
|
| 162 |
-
else:
|
| 163 |
-
cur.changes.append(RawFileChange(
|
| 164 |
-
status=status, new_blob=new_blob, path=parts[0]))
|
| 165 |
-
except Exception:
|
| 166 |
-
continue
|
| 167 |
-
if cur is not None:
|
| 168 |
-
yield cur
|
| 169 |
-
finally:
|
| 170 |
-
try:
|
| 171 |
-
proc.stdout.close()
|
| 172 |
-
finally:
|
| 173 |
-
proc.wait(timeout=5)
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
class GitCatFileBatch:
|
| 177 |
-
"""Persistent ``git cat-file --batch`` for cheap blob reads."""
|
| 178 |
-
|
| 179 |
-
def __init__(self, repo_dir: Path):
|
| 180 |
-
self._proc = subprocess.Popen(
|
| 181 |
-
["git", "-C", str(repo_dir), "cat-file", "--batch"],
|
| 182 |
-
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
| 183 |
-
stderr=subprocess.PIPE, bufsize=0,
|
| 184 |
-
)
|
| 185 |
-
|
| 186 |
-
def read_blob(self, blob_sha: str) -> Optional[bytes]:
|
| 187 |
-
if not blob_sha or blob_sha == _NULL_SHA:
|
| 188 |
-
return None
|
| 189 |
-
p = self._proc
|
| 190 |
-
assert p.stdin is not None and p.stdout is not None
|
| 191 |
-
try:
|
| 192 |
-
p.stdin.write((blob_sha + "\n").encode("ascii"))
|
| 193 |
-
p.stdin.flush()
|
| 194 |
-
except (BrokenPipeError, OSError):
|
| 195 |
-
return None
|
| 196 |
-
header = p.stdout.readline()
|
| 197 |
-
if not header:
|
| 198 |
-
return None
|
| 199 |
-
parts = header.decode("utf-8", errors="replace").strip().split()
|
| 200 |
-
if len(parts) < 3 or parts[1] != "blob":
|
| 201 |
-
return None
|
| 202 |
-
try:
|
| 203 |
-
size = int(parts[2])
|
| 204 |
-
except ValueError:
|
| 205 |
-
return None
|
| 206 |
-
body = b""
|
| 207 |
-
remaining = size
|
| 208 |
-
while remaining > 0:
|
| 209 |
-
chunk = p.stdout.read(remaining)
|
| 210 |
-
if not chunk:
|
| 211 |
-
break
|
| 212 |
-
body += chunk
|
| 213 |
-
remaining -= len(chunk)
|
| 214 |
-
try:
|
| 215 |
-
p.stdout.read(1)
|
| 216 |
-
except Exception:
|
| 217 |
-
pass
|
| 218 |
-
return body
|
| 219 |
-
|
| 220 |
-
def read_blob_text(self, blob_sha: str) -> Optional[str]:
|
| 221 |
-
data = self.read_blob(blob_sha)
|
| 222 |
-
return None if data is None else data.decode("utf-8", errors="replace")
|
| 223 |
-
|
| 224 |
-
def close(self) -> None:
|
| 225 |
-
try:
|
| 226 |
-
if self._proc.stdin is not None:
|
| 227 |
-
self._proc.stdin.close()
|
| 228 |
-
except Exception:
|
| 229 |
-
pass
|
| 230 |
-
try:
|
| 231 |
-
self._proc.wait(timeout=5)
|
| 232 |
-
except Exception:
|
| 233 |
-
try:
|
| 234 |
-
self._proc.kill()
|
| 235 |
-
except Exception:
|
| 236 |
-
pass
|
| 237 |
-
|
| 238 |
-
def __enter__(self):
|
| 239 |
-
return self
|
| 240 |
-
|
| 241 |
-
def __exit__(self, *exc):
|
| 242 |
-
self.close()
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
def get_diff(repo_dir: Path, parent_sha: Optional[str], child_sha: str,
|
| 246 |
-
timeout: int = 600) -> str:
|
| 247 |
-
base = parent_sha if parent_sha else EMPTY_TREE_SHA
|
| 248 |
-
out = _run_git(repo_dir, ["diff", "--no-color", "-U3", base, child_sha],
|
| 249 |
-
timeout=timeout)
|
| 250 |
-
return out if out else ""
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
_DIFF_HEADER_RE = re.compile(r"^diff --git a/(.+?) b/(.+?)(?:\n|$)", re.MULTILINE)
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
def filter_diff(raw_diff: str) -> str:
|
| 257 |
-
if not raw_diff:
|
| 258 |
-
return ""
|
| 259 |
-
chunks = re.split(r"(?=^diff --git )", raw_diff, flags=re.MULTILINE)
|
| 260 |
-
kept: List[str] = []
|
| 261 |
-
for chunk in chunks:
|
| 262 |
-
if not chunk.startswith("diff --git "):
|
| 263 |
-
continue
|
| 264 |
-
m = _DIFF_HEADER_RE.match(chunk)
|
| 265 |
-
if not m:
|
| 266 |
-
continue
|
| 267 |
-
if _should_skip_diff_path(m.group(2)):
|
| 268 |
-
continue
|
| 269 |
-
kept.append(chunk)
|
| 270 |
-
return "".join(kept)
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
def ls_tree_py(repo_dir: Path, commit_sha: str,
|
| 274 |
-
max_file_bytes: int = 2_000_000) -> List[Tuple[str, str, int]]:
|
| 275 |
-
"""Return [(blob_sha, path, size)] for tracked .py files at a commit."""
|
| 276 |
-
out = _run_git(repo_dir, ["ls-tree", "-r", "-l", commit_sha], timeout=120)
|
| 277 |
-
if not out:
|
| 278 |
-
return []
|
| 279 |
-
rows: List[Tuple[str, str, int]] = []
|
| 280 |
-
for ln in out.splitlines():
|
| 281 |
-
try:
|
| 282 |
-
meta, fpath = ln.split("\t", 1)
|
| 283 |
-
_mode, ftype, blob, size_str = meta.split()
|
| 284 |
-
except ValueError:
|
| 285 |
-
continue
|
| 286 |
-
if ftype != "blob" or not fpath.endswith(".py"):
|
| 287 |
-
continue
|
| 288 |
-
try:
|
| 289 |
-
size = int(size_str)
|
| 290 |
-
except ValueError:
|
| 291 |
-
continue
|
| 292 |
-
if size <= 0 or size > max_file_bytes:
|
| 293 |
-
continue
|
| 294 |
-
if any(part in SKIP_DIRS for part in Path(fpath).parts):
|
| 295 |
-
continue
|
| 296 |
-
rows.append((blob, fpath, size))
|
| 297 |
-
return rows
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
# ---------------------------------------------------------------------------
|
| 301 |
-
# Chronological split
|
| 302 |
-
# ---------------------------------------------------------------------------
|
| 303 |
-
|
| 304 |
-
def chronological_splits(n_kept: int) -> List[str]:
|
| 305 |
-
"""80/10/10 chronological split of ``n_kept`` kept commits."""
|
| 306 |
-
if n_kept <= 0:
|
| 307 |
-
return []
|
| 308 |
-
if n_kept < 3:
|
| 309 |
-
return ["train"] * n_kept
|
| 310 |
-
n_train = max(1, int(n_kept * 0.8))
|
| 311 |
-
n_val = max(1, int(n_kept * 0.1))
|
| 312 |
-
n_test = n_kept - n_train - n_val
|
| 313 |
-
if n_test < 1:
|
| 314 |
-
n_val = max(1, n_val - 1) if n_val > 1 else n_val
|
| 315 |
-
n_test = n_kept - n_train - n_val
|
| 316 |
-
if n_test < 1:
|
| 317 |
-
n_train = max(1, n_train - 1)
|
| 318 |
-
n_test = n_kept - n_train - n_val
|
| 319 |
-
return ["train"] * n_train + ["val"] * n_val + ["test"] * n_test
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
# ---------------------------------------------------------------------------
|
| 323 |
-
# Per-repo processing
|
| 324 |
-
# ---------------------------------------------------------------------------
|
| 325 |
-
|
| 326 |
-
@dataclass
|
| 327 |
-
class QnaItem:
|
| 328 |
-
test_file: str
|
| 329 |
-
test_function: str
|
| 330 |
-
assertion_type: str
|
| 331 |
-
prefix: str
|
| 332 |
-
target: str
|
| 333 |
-
lineno: int
|
| 334 |
-
col_offset: int
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
@dataclass
|
| 338 |
-
class KeptCommit:
|
| 339 |
-
commit_index: int # index among kept commits (0-based)
|
| 340 |
-
original_index: int # index in the full first-parent history
|
| 341 |
-
sha: str
|
| 342 |
-
timestamp: str
|
| 343 |
-
in_repo_split: str # train / val / test
|
| 344 |
-
production_code_diff: str
|
| 345 |
-
n_new_assertions: int
|
| 346 |
-
qnas: List[QnaItem] = field(default_factory=list)
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
@dataclass
|
| 350 |
-
class RepoProcessResult:
|
| 351 |
-
repo_id: str
|
| 352 |
-
total_commits: int # all first-parent commits
|
| 353 |
-
test_touching_commits: int # commits that changed a test file
|
| 354 |
-
kept_commits: int # commits selected for the GRU pass
|
| 355 |
-
n_assertions: int # total QnA pairs emitted
|
| 356 |
-
n_train_kept: int
|
| 357 |
-
n_val_kept: int
|
| 358 |
-
n_test_kept: int
|
| 359 |
-
commits: List[KeptCommit]
|
| 360 |
-
|
| 361 |
-
@property
|
| 362 |
-
def split_boundary_index(self) -> int:
|
| 363 |
-
"""Index (within kept commits) of the last train commit (the 80% point)."""
|
| 364 |
-
last_train = -1
|
| 365 |
-
for c in self.commits:
|
| 366 |
-
if c.in_repo_split == "train":
|
| 367 |
-
last_train = c.commit_index
|
| 368 |
-
return last_train
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
ProgressFn = Optional[Callable[[str, float], None]]
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
def process_repo(repo_dir: Path, repo_id: str = "",
|
| 375 |
-
diff_timeout: int = 600,
|
| 376 |
-
progress: ProgressFn = None) -> RepoProcessResult:
|
| 377 |
-
"""Process a local clone into the kept-commit sequence + QnAs + stats."""
|
| 378 |
-
prev_assert_keys: Dict[str, Set[Tuple[str, str, str]]] = {}
|
| 379 |
-
kept: List[Dict] = []
|
| 380 |
-
total_commits = 0
|
| 381 |
-
test_touching = 0
|
| 382 |
-
|
| 383 |
-
def _report(msg: str, frac: float) -> None:
|
| 384 |
-
if progress is not None:
|
| 385 |
-
progress(msg, frac)
|
| 386 |
-
|
| 387 |
-
# ---- Phase 1: detect kept commits ----
|
| 388 |
-
with GitCatFileBatch(repo_dir) as cat:
|
| 389 |
-
for ci, raw in enumerate(stream_commit_changes(repo_dir)):
|
| 390 |
-
total_commits += 1
|
| 391 |
-
if ci % 50 == 0:
|
| 392 |
-
_report(f"Scanning commit history… ({total_commits} commits)", 0.0)
|
| 393 |
-
|
| 394 |
-
candidate_changes: List[RawFileChange] = []
|
| 395 |
-
for ch in raw.changes:
|
| 396 |
-
if ch.old_path and ch.path != ch.old_path:
|
| 397 |
-
if ch.old_path in prev_assert_keys:
|
| 398 |
-
prev_assert_keys[ch.path] = prev_assert_keys.pop(ch.old_path)
|
| 399 |
-
if ch.status.startswith("D"):
|
| 400 |
-
prev_assert_keys.pop(ch.path, None)
|
| 401 |
-
continue
|
| 402 |
-
if _is_python_test_file(ch.path):
|
| 403 |
-
candidate_changes.append(ch)
|
| 404 |
-
|
| 405 |
-
if not candidate_changes:
|
| 406 |
-
continue
|
| 407 |
-
test_touching += 1
|
| 408 |
-
|
| 409 |
-
per_file_new: List[Tuple[str, List[ExtractedAssertion]]] = []
|
| 410 |
-
for ch in candidate_changes:
|
| 411 |
-
content = cat.read_blob_text(ch.new_blob)
|
| 412 |
-
if content is None:
|
| 413 |
-
prev_assert_keys.pop(ch.path, None)
|
| 414 |
-
continue
|
| 415 |
-
extracted = extract_assertions_from_source(content)
|
| 416 |
-
current_keys: Set[Tuple[str, str, str]] = set()
|
| 417 |
-
for ex in extracted:
|
| 418 |
-
current_keys.add((
|
| 419 |
-
ex.assertion_type,
|
| 420 |
-
normalize_for_id(ex.prefix),
|
| 421 |
-
normalize_for_id(ex.target),
|
| 422 |
-
))
|
| 423 |
-
prev_keys = prev_assert_keys.get(ch.path, set())
|
| 424 |
-
new_keys = current_keys - prev_keys
|
| 425 |
-
if new_keys:
|
| 426 |
-
new_extracted = [
|
| 427 |
-
ex for ex in extracted
|
| 428 |
-
if (ex.assertion_type, normalize_for_id(ex.prefix),
|
| 429 |
-
normalize_for_id(ex.target)) in new_keys
|
| 430 |
-
]
|
| 431 |
-
if new_extracted:
|
| 432 |
-
per_file_new.append((ch.path, new_extracted))
|
| 433 |
-
prev_assert_keys[ch.path] = current_keys
|
| 434 |
-
|
| 435 |
-
if not per_file_new:
|
| 436 |
-
continue
|
| 437 |
-
kept.append({
|
| 438 |
-
"original_idx": ci,
|
| 439 |
-
"sha": raw.sha,
|
| 440 |
-
"timestamp": raw.timestamp,
|
| 441 |
-
"per_file_new": per_file_new,
|
| 442 |
-
})
|
| 443 |
-
|
| 444 |
-
if not kept:
|
| 445 |
-
return RepoProcessResult(
|
| 446 |
-
repo_id=repo_id, total_commits=total_commits,
|
| 447 |
-
test_touching_commits=test_touching, kept_commits=0,
|
| 448 |
-
n_assertions=0, n_train_kept=0, n_val_kept=0, n_test_kept=0,
|
| 449 |
-
commits=[])
|
| 450 |
-
|
| 451 |
-
# ---- Phase 2: build diffs + chronological split ----
|
| 452 |
-
splits = chronological_splits(len(kept))
|
| 453 |
-
commits: List[KeptCommit] = []
|
| 454 |
-
prev_kept_sha: Optional[str] = None
|
| 455 |
-
n_assertions = 0
|
| 456 |
-
for ki, kc in enumerate(kept):
|
| 457 |
-
_report(f"Building production-code diffs… ({ki + 1}/{len(kept)} kept commits)",
|
| 458 |
-
(ki + 1) / max(len(kept), 1))
|
| 459 |
-
sha = kc["sha"]
|
| 460 |
-
raw_diff = get_diff(repo_dir, prev_kept_sha, sha, timeout=diff_timeout)
|
| 461 |
-
filtered = filter_diff(raw_diff) if raw_diff else ""
|
| 462 |
-
|
| 463 |
-
qnas: List[QnaItem] = []
|
| 464 |
-
for tfile, extracted in kc["per_file_new"]:
|
| 465 |
-
for ex in extracted:
|
| 466 |
-
qnas.append(QnaItem(
|
| 467 |
-
test_file=tfile, test_function=ex.test_function,
|
| 468 |
-
assertion_type=ex.assertion_type, prefix=ex.prefix,
|
| 469 |
-
target=ex.target, lineno=ex.lineno, col_offset=ex.col_offset))
|
| 470 |
-
n_assertions += len(qnas)
|
| 471 |
-
|
| 472 |
-
commits.append(KeptCommit(
|
| 473 |
-
commit_index=ki, original_index=kc["original_idx"], sha=sha,
|
| 474 |
-
timestamp=kc.get("timestamp", ""), in_repo_split=splits[ki],
|
| 475 |
-
production_code_diff=filtered, n_new_assertions=len(qnas), qnas=qnas))
|
| 476 |
-
prev_kept_sha = sha
|
| 477 |
-
|
| 478 |
-
n_train = sum(1 for s in splits if s == "train")
|
| 479 |
-
n_val = sum(1 for s in splits if s == "val")
|
| 480 |
-
n_test = sum(1 for s in splits if s == "test")
|
| 481 |
-
return RepoProcessResult(
|
| 482 |
-
repo_id=repo_id, total_commits=total_commits,
|
| 483 |
-
test_touching_commits=test_touching, kept_commits=len(kept),
|
| 484 |
-
n_assertions=n_assertions, n_train_kept=n_train, n_val_kept=n_val,
|
| 485 |
-
n_test_kept=n_test, commits=commits)
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
__all__ = [
|
| 489 |
-
"process_repo",
|
| 490 |
-
"RepoProcessResult",
|
| 491 |
-
"KeptCommit",
|
| 492 |
-
"QnaItem",
|
| 493 |
-
"ls_tree_py",
|
| 494 |
-
"GitCatFileBatch",
|
| 495 |
-
"get_diff",
|
| 496 |
-
"filter_diff",
|
| 497 |
-
"chronological_splits",
|
| 498 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c2l_demo/viz.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
"""Plotting helpers for the demo (matplotlib, Agg backend).
|
| 2 |
-
|
| 3 |
-
Three figures:
|
| 4 |
-
|
| 5 |
-
* :func:`plot_commit_timeline` -- the full commit history with the kept
|
| 6 |
-
"GRU-pass" commits highlighted, dot size proportional to the number of new
|
| 7 |
-
assertions, colored by the chronological 80/10/10 split.
|
| 8 |
-
* :func:`plot_gru_processing` -- the GRU hidden-state trajectory (2-D PCA) as
|
| 9 |
-
it streams through the repository, plus the context-norm curve, marking the
|
| 10 |
-
adapter (80%) point.
|
| 11 |
-
* :func:`plot_assertion_type_breakdown` -- distribution of assertion kinds.
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
from __future__ import annotations
|
| 15 |
-
|
| 16 |
-
from datetime import datetime
|
| 17 |
-
from typing import List, Optional
|
| 18 |
-
|
| 19 |
-
import matplotlib
|
| 20 |
-
|
| 21 |
-
matplotlib.use("Agg")
|
| 22 |
-
import matplotlib.dates as mdates # noqa: E402
|
| 23 |
-
import matplotlib.pyplot as plt # noqa: E402
|
| 24 |
-
import numpy as np # noqa: E402
|
| 25 |
-
|
| 26 |
-
# Modern, conference-grade palette
|
| 27 |
-
_BG = "#0f1117"
|
| 28 |
-
_FG = "#e6e6e6"
|
| 29 |
-
_GRID = "#2a2e3a"
|
| 30 |
-
_TRAIN = "#4f8cff"
|
| 31 |
-
_VAL = "#ffcc4d"
|
| 32 |
-
_TEST = "#ff6b6b"
|
| 33 |
-
_ACCENT = "#22d3a6"
|
| 34 |
-
|
| 35 |
-
_SPLIT_COLORS = {"train": _TRAIN, "val": _VAL, "test": _TEST}
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _style(ax):
|
| 39 |
-
ax.set_facecolor(_BG)
|
| 40 |
-
for spine in ax.spines.values():
|
| 41 |
-
spine.set_color(_GRID)
|
| 42 |
-
ax.tick_params(colors=_FG, labelsize=9)
|
| 43 |
-
ax.yaxis.label.set_color(_FG)
|
| 44 |
-
ax.xaxis.label.set_color(_FG)
|
| 45 |
-
ax.title.set_color(_FG)
|
| 46 |
-
ax.grid(True, color=_GRID, linewidth=0.6, alpha=0.6)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def _parse_ts(ts: str) -> Optional[datetime]:
|
| 50 |
-
if not ts:
|
| 51 |
-
return None
|
| 52 |
-
try:
|
| 53 |
-
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
| 54 |
-
except ValueError:
|
| 55 |
-
return None
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def plot_commit_timeline(result, endpoint_index: int):
|
| 59 |
-
"""Scatter of kept commits over real time; size = #new assertions."""
|
| 60 |
-
commits = result.commits
|
| 61 |
-
fig, ax = plt.subplots(figsize=(10, 4.2), dpi=120)
|
| 62 |
-
fig.patch.set_facecolor(_BG)
|
| 63 |
-
_style(ax)
|
| 64 |
-
|
| 65 |
-
xs, ys, sizes, colors = [], [], [], []
|
| 66 |
-
parsed = [_parse_ts(c.timestamp) for c in commits]
|
| 67 |
-
distinct_dates = {p for p in parsed if p is not None}
|
| 68 |
-
use_dates = (all(p is not None for p in parsed) and len(distinct_dates) > 1)
|
| 69 |
-
for i, c in enumerate(commits):
|
| 70 |
-
x = parsed[i] if use_dates else c.commit_index
|
| 71 |
-
xs.append(x)
|
| 72 |
-
ys.append(c.n_new_assertions)
|
| 73 |
-
sizes.append(20 + 22 * np.sqrt(max(c.n_new_assertions, 0)))
|
| 74 |
-
colors.append(_SPLIT_COLORS.get(c.in_repo_split, _FG))
|
| 75 |
-
|
| 76 |
-
ax.scatter(xs, ys, s=sizes, c=colors, alpha=0.85, edgecolors="black",
|
| 77 |
-
linewidths=0.4, zorder=3)
|
| 78 |
-
|
| 79 |
-
# Mark the adapter (80%) boundary.
|
| 80 |
-
if 0 <= endpoint_index < len(commits):
|
| 81 |
-
bx = xs[endpoint_index]
|
| 82 |
-
ax.axvline(bx, color=_ACCENT, linestyle="--", linewidth=1.6, alpha=0.9,
|
| 83 |
-
zorder=2)
|
| 84 |
-
ax.text(bx, ax.get_ylim()[1] * 0.92, " adapter @ 80%",
|
| 85 |
-
color=_ACCENT, fontsize=9, va="top", ha="left")
|
| 86 |
-
|
| 87 |
-
if use_dates:
|
| 88 |
-
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
|
| 89 |
-
fig.autofmt_xdate(rotation=25)
|
| 90 |
-
ax.set_xlabel("commit date")
|
| 91 |
-
else:
|
| 92 |
-
ax.set_xlabel("kept-commit index")
|
| 93 |
-
ax.set_ylabel("new assertions in commit")
|
| 94 |
-
ax.set_title(f"{result.repo_id} · {result.kept_commits} GRU-pass commits "
|
| 95 |
-
f"· {result.n_assertions} assertions", fontsize=11)
|
| 96 |
-
|
| 97 |
-
handles = [
|
| 98 |
-
plt.Line2D([0], [0], marker="o", color=_BG, markerfacecolor=col,
|
| 99 |
-
markersize=8, label=lab, markeredgecolor="black")
|
| 100 |
-
for lab, col in (("train (≤80%)", _TRAIN), ("val", _VAL), ("test", _TEST))
|
| 101 |
-
]
|
| 102 |
-
leg = ax.legend(handles=handles, loc="upper left", fontsize=8,
|
| 103 |
-
facecolor=_BG, edgecolor=_GRID, labelcolor=_FG)
|
| 104 |
-
leg.get_frame().set_alpha(0.7)
|
| 105 |
-
fig.tight_layout()
|
| 106 |
-
return fig
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def plot_gru_processing(session):
|
| 110 |
-
"""2-D PCA trajectory of the GRU hidden state + context-norm curve."""
|
| 111 |
-
H = session.hidden_states # [T, H]
|
| 112 |
-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10.5, 4.3), dpi=120,
|
| 113 |
-
gridspec_kw={"width_ratios": [1.25, 1]})
|
| 114 |
-
fig.patch.set_facecolor(_BG)
|
| 115 |
-
_style(ax1)
|
| 116 |
-
_style(ax2)
|
| 117 |
-
|
| 118 |
-
T = H.shape[0]
|
| 119 |
-
if T >= 2:
|
| 120 |
-
Hc = H - H.mean(axis=0, keepdims=True)
|
| 121 |
-
try:
|
| 122 |
-
_, _, vt = np.linalg.svd(Hc, full_matrices=False)
|
| 123 |
-
proj = Hc @ vt[:2].T # [T, 2]
|
| 124 |
-
except np.linalg.LinAlgError:
|
| 125 |
-
proj = Hc[:, :2]
|
| 126 |
-
order = np.arange(T)
|
| 127 |
-
ax1.plot(proj[:, 0], proj[:, 1], color=_GRID, linewidth=1.0, zorder=1,
|
| 128 |
-
alpha=0.8)
|
| 129 |
-
sc = ax1.scatter(proj[:, 0], proj[:, 1], c=order, cmap="viridis",
|
| 130 |
-
s=34, zorder=3, edgecolors="black", linewidths=0.3)
|
| 131 |
-
ax1.scatter(proj[0, 0], proj[0, 1], marker="o", s=130,
|
| 132 |
-
facecolors="none", edgecolors=_FG, linewidths=1.6,
|
| 133 |
-
zorder=4, label="commit 0")
|
| 134 |
-
ax1.scatter(proj[-1, 0], proj[-1, 1], marker="*", s=240, color=_ACCENT,
|
| 135 |
-
edgecolors="black", linewidths=0.5, zorder=5,
|
| 136 |
-
label="adapter point")
|
| 137 |
-
cbar = fig.colorbar(sc, ax=ax1, fraction=0.046, pad=0.04)
|
| 138 |
-
cbar.set_label("commit order", color=_FG, fontsize=8)
|
| 139 |
-
cbar.ax.tick_params(colors=_FG, labelsize=8)
|
| 140 |
-
ax1.legend(loc="best", fontsize=8, facecolor=_BG, edgecolor=_GRID,
|
| 141 |
-
labelcolor=_FG)
|
| 142 |
-
else:
|
| 143 |
-
ax1.text(0.5, 0.5, "Not enough commits\nfor a trajectory",
|
| 144 |
-
color=_FG, ha="center", va="center", transform=ax1.transAxes)
|
| 145 |
-
ax1.set_title("GRU hidden state walking the repository (PCA)", fontsize=10.5)
|
| 146 |
-
ax1.set_xlabel("PC 1")
|
| 147 |
-
ax1.set_ylabel("PC 2")
|
| 148 |
-
|
| 149 |
-
# Context-norm curve.
|
| 150 |
-
if session.ctx_norms:
|
| 151 |
-
ax2.plot(session.walked_indices, session.ctx_norms, color=_ACCENT,
|
| 152 |
-
linewidth=1.8)
|
| 153 |
-
ax2.fill_between(session.walked_indices, session.ctx_norms,
|
| 154 |
-
color=_ACCENT, alpha=0.12)
|
| 155 |
-
ax2.set_title("repository knowledge accumulating", fontsize=10.5)
|
| 156 |
-
ax2.set_xlabel("kept-commit index")
|
| 157 |
-
ax2.set_ylabel("‖adapter context‖")
|
| 158 |
-
fig.tight_layout()
|
| 159 |
-
return fig
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def plot_assertion_type_breakdown(result):
|
| 163 |
-
"""Horizontal bar chart of assertion kinds across all QnAs."""
|
| 164 |
-
counts: dict = {}
|
| 165 |
-
for c in result.commits:
|
| 166 |
-
for q in c.qnas:
|
| 167 |
-
counts[q.assertion_type] = counts.get(q.assertion_type, 0) + 1
|
| 168 |
-
fig, ax = plt.subplots(figsize=(10.5, max(2.2, 0.42 * max(len(counts), 1))),
|
| 169 |
-
dpi=120)
|
| 170 |
-
fig.patch.set_facecolor(_BG)
|
| 171 |
-
_style(ax)
|
| 172 |
-
if not counts:
|
| 173 |
-
ax.text(0.5, 0.5, "No assertions", color=_FG, ha="center",
|
| 174 |
-
va="center", transform=ax.transAxes)
|
| 175 |
-
return fig
|
| 176 |
-
items = sorted(counts.items(), key=lambda kv: kv[1])
|
| 177 |
-
labels = [k for k, _ in items]
|
| 178 |
-
vals = [v for _, v in items]
|
| 179 |
-
ax.barh(labels, vals, color=_TRAIN, edgecolor="black", linewidth=0.4)
|
| 180 |
-
for i, v in enumerate(vals):
|
| 181 |
-
ax.text(v, i, f" {v}", color=_FG, va="center", fontsize=8)
|
| 182 |
-
ax.set_title("assertion types extracted from the test suite", fontsize=10.5)
|
| 183 |
-
ax.set_xlabel("count")
|
| 184 |
-
fig.tight_layout()
|
| 185 |
-
return fig
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
__all__ = [
|
| 189 |
-
"plot_commit_timeline",
|
| 190 |
-
"plot_gru_processing",
|
| 191 |
-
"plot_assertion_type_breakdown",
|
| 192 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,9 +1,7 @@
|
|
| 1 |
-
|
| 2 |
torch>=2.3
|
| 3 |
-
transformers>=4.45
|
| 4 |
accelerate>=0.34
|
| 5 |
-
huggingface_hub>=0.
|
| 6 |
numpy>=1.24
|
| 7 |
matplotlib>=3.7
|
| 8 |
-
# optional, improves CodeBLEU fidelity if present:
|
| 9 |
-
# codebleu>=0.7
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
torch>=2.3
|
| 3 |
+
transformers>=4.45,<5
|
| 4 |
accelerate>=0.34
|
| 5 |
+
huggingface_hub>=0.28
|
| 6 |
numpy>=1.24
|
| 7 |
matplotlib>=3.7
|
|
|
|
|
|
tools/build_space_data.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Package the precompute cache into the small files the Space reads.
|
| 3 |
+
|
| 4 |
+
python hf_space_demo/tools/build_space_data.py \
|
| 5 |
+
--cache-dir /scratch/lhotsko/C2L_SPACE_CACHE --out-dir hf_space_demo/data
|
| 6 |
+
|
| 7 |
+
Writes ``index.json`` plus, per selected repository, ``<safe>.json.gz``
|
| 8 |
+
(commits + assertions + cached predictions) and ``<safe>.ctx.npy``
|
| 9 |
+
(float32 [n_commits, 2048] GRU contexts).
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import gzip
|
| 16 |
+
import json
|
| 17 |
+
import pickle
|
| 18 |
+
import urllib.request
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def github_meta(repo_id: str) -> dict:
|
| 25 |
+
try:
|
| 26 |
+
with urllib.request.urlopen(f"https://api.github.com/repos/{repo_id}", timeout=10) as r:
|
| 27 |
+
d = json.load(r)
|
| 28 |
+
return {"description": d.get("description") or "", "stars": d.get("stargazers_count")}
|
| 29 |
+
except Exception:
|
| 30 |
+
return {"description": "", "stars": None}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main() -> None:
|
| 34 |
+
ap = argparse.ArgumentParser()
|
| 35 |
+
ap.add_argument("--cache-dir", required=True)
|
| 36 |
+
ap.add_argument("--out-dir", required=True)
|
| 37 |
+
ap.add_argument("--min-commits", type=int, default=10)
|
| 38 |
+
ap.add_argument("--max-commits", type=int, default=400)
|
| 39 |
+
ap.add_argument("--max-repos", type=int, default=24)
|
| 40 |
+
args = ap.parse_args()
|
| 41 |
+
|
| 42 |
+
out = Path(args.out_dir)
|
| 43 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 44 |
+
candidates = []
|
| 45 |
+
for p in sorted(Path(args.cache_dir).glob("*.pkl")):
|
| 46 |
+
with open(p, "rb") as f:
|
| 47 |
+
d = pickle.load(f)
|
| 48 |
+
n_c, qn = len(d["commits"]), d["qnas"]
|
| 49 |
+
if not (args.min_commits <= n_c <= args.max_commits) or not qn:
|
| 50 |
+
continue
|
| 51 |
+
base = sum(q["base_em"] for q in qn) / len(qn)
|
| 52 |
+
c2l = sum(q["c2l_em"] for q in qn) / len(qn)
|
| 53 |
+
tt = sum(1 for q in qn if q.get("prev_em") == 0.0)
|
| 54 |
+
candidates.append((c2l - base, tt, p, d, base, c2l))
|
| 55 |
+
|
| 56 |
+
candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
| 57 |
+
index = []
|
| 58 |
+
for gain, tt, p, d, base, c2l in candidates[: args.max_repos]:
|
| 59 |
+
safe = p.stem
|
| 60 |
+
commits = d["commits"]
|
| 61 |
+
per_commit = [[] for _ in commits]
|
| 62 |
+
for q in d["qnas"]:
|
| 63 |
+
per_commit[q["k"]].append(q)
|
| 64 |
+
for k, c in enumerate(commits):
|
| 65 |
+
qs = per_commit[k]
|
| 66 |
+
c["n_qnas"] = len(qs)
|
| 67 |
+
c["base_em"] = sum(q["base_em"] for q in qs) / len(qs) if qs else None
|
| 68 |
+
c["c2l_em"] = sum(q["c2l_em"] for q in qs) / len(qs) if qs else None
|
| 69 |
+
c["time_travel"] = any(q.get("prev_em") == 0.0 for q in qs)
|
| 70 |
+
payload = {"repo_id": d["repo_id"], "commits": commits, "qnas": per_commit,
|
| 71 |
+
"max_input_tokens": d["max_input_tokens"],
|
| 72 |
+
"max_new_tokens": d["max_new_tokens"]}
|
| 73 |
+
with gzip.open(out / f"{safe}.json.gz", "wt", encoding="utf-8") as f:
|
| 74 |
+
json.dump(payload, f)
|
| 75 |
+
np.save(out / f"{safe}.ctx.npy", d["ctx"].astype(np.float32))
|
| 76 |
+
dates = [c.get("timestamp") for c in commits if c.get("timestamp")]
|
| 77 |
+
index.append({
|
| 78 |
+
"repo_id": d["repo_id"], "safe": safe, "n_commits": len(commits),
|
| 79 |
+
"n_qnas": len(d["qnas"]), "base_em": base, "c2l_em": c2l,
|
| 80 |
+
"n_time_travel": tt, "first_date": min(dates) if dates else "",
|
| 81 |
+
"last_date": max(dates) if dates else "", **github_meta(d["repo_id"]),
|
| 82 |
+
})
|
| 83 |
+
print(f"{d['repo_id']:45s} commits={len(commits):4d} base={base:.3f} "
|
| 84 |
+
f"c2l={c2l:.3f} gain={gain:+.3f} time_travel={tt}")
|
| 85 |
+
|
| 86 |
+
index.sort(key=lambda r: r["repo_id"].lower())
|
| 87 |
+
(out / "index.json").write_text(json.dumps(index, indent=1))
|
| 88 |
+
print(f"wrote {len(index)} repos to {out}")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
main()
|
tools/precompute.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Precompute everything the Code2LoRA timeline Space serves from cache.
|
| 3 |
+
|
| 4 |
+
For every cross-repo test repository (held out from training) this walks the
|
| 5 |
+
Code2LoRA-Evo GRU over the precomputed per-commit embeddings and records:
|
| 6 |
+
|
| 7 |
+
* ``ctx`` -- output_norm(h_t) after every commit (what the head consumes),
|
| 8 |
+
* for every QnA attached to a commit (first <=8, the paper's eval cap):
|
| 9 |
+
- the base-model prediction (no adapter),
|
| 10 |
+
- the prediction with the adapter generated at that commit (paper protocol),
|
| 11 |
+
- for QnAs the adapter solves and the base model does not, the prediction
|
| 12 |
+
with the adapter from the *previous* commit ("time travel").
|
| 13 |
+
|
| 14 |
+
One pickle per repository is written to ``--out-dir``; existing files are
|
| 15 |
+
skipped, so the job is resumable. Run from the RepoPeftData root:
|
| 16 |
+
|
| 17 |
+
python hf_space_demo/tools/precompute.py --out-dir $SCRATCH/C2L_SPACE_CACHE \
|
| 18 |
+
--shard-i 0 --num-shards 8
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import pickle
|
| 25 |
+
import sys
|
| 26 |
+
import time
|
| 27 |
+
from collections import defaultdict
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from typing import Dict, List, Optional
|
| 30 |
+
|
| 31 |
+
import numpy as np
|
| 32 |
+
import pyarrow.parquet as pq
|
| 33 |
+
import torch
|
| 34 |
+
|
| 35 |
+
_ROOT = Path(__file__).resolve().parents[2]
|
| 36 |
+
for p in (_ROOT, _ROOT / "hypernetwork"):
|
| 37 |
+
if str(p) not in sys.path:
|
| 38 |
+
sys.path.insert(0, str(p))
|
| 39 |
+
|
| 40 |
+
from code2lora_core import ( # noqa: E402
|
| 41 |
+
LoRA,
|
| 42 |
+
inject_lora_weights,
|
| 43 |
+
load_commit_rows_for_gru,
|
| 44 |
+
load_qna_rows,
|
| 45 |
+
)
|
| 46 |
+
from evaluation.metrics import compute_metrics # noqa: E402
|
| 47 |
+
from evaluation.run_code2lora_gru_v2_eval import ( # noqa: E402
|
| 48 |
+
DEFAULT_BASE_MODEL,
|
| 49 |
+
DEFAULT_TARGET_MODULES,
|
| 50 |
+
_generate_batch,
|
| 51 |
+
_get_bos_id,
|
| 52 |
+
_load_v2_gru_ckpt,
|
| 53 |
+
_prepare_prefix_ids,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
COMMITS = "/scratch/lhotsko/REPO_DATASET/commit_parquet_hf_v2/commits/cr_test.parquet"
|
| 57 |
+
QNAS = "/scratch/lhotsko/REPO_DATASET/code2lora_snapshots_hf/qna/cr_test.parquet"
|
| 58 |
+
CKPT = "/scratch/lhotsko/hf_staging/code2lora_gru/gru_head.best.pt"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def clear_all_lora_weights(model: torch.nn.Module, specs) -> None:
|
| 62 |
+
named = dict(model.named_modules())
|
| 63 |
+
for sp in specs:
|
| 64 |
+
mod = named[sp.full_name]
|
| 65 |
+
if isinstance(mod, LoRA):
|
| 66 |
+
mod.A, mod.B = None, None
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def safe_name(repo_id: str) -> str:
|
| 70 |
+
return repo_id.replace("/", "__")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def commit_metadata(repo_ids: List[str]) -> Dict[str, Dict[str, dict]]:
|
| 74 |
+
t = pq.read_table(COMMITS, columns=[
|
| 75 |
+
"repo_id", "commit_sha", "commit_index", "commit_timestamp",
|
| 76 |
+
"n_new_assertions", "production_code_diff"],
|
| 77 |
+
filters=[("repo_id", "in", repo_ids)])
|
| 78 |
+
out: Dict[str, Dict[str, dict]] = defaultdict(dict)
|
| 79 |
+
for r in t.to_pylist():
|
| 80 |
+
diff = r["production_code_diff"] or ""
|
| 81 |
+
out[r["repo_id"]][r["commit_sha"]] = {
|
| 82 |
+
"timestamp": r["commit_timestamp"],
|
| 83 |
+
"n_new_assertions": int(r["n_new_assertions"] or 0),
|
| 84 |
+
"diff_preview": diff[:6000],
|
| 85 |
+
"diff_chars": len(diff),
|
| 86 |
+
}
|
| 87 |
+
return out
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def score(pred: str, target: str) -> Dict[str, float]:
|
| 91 |
+
m = compute_metrics(pred, target)
|
| 92 |
+
return {"em": float(bool(m["exact_match"])), "es": float(m["edit_similarity"])}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@torch.no_grad()
|
| 96 |
+
def main() -> None:
|
| 97 |
+
ap = argparse.ArgumentParser()
|
| 98 |
+
ap.add_argument("--out-dir", required=True)
|
| 99 |
+
ap.add_argument("--checkpoint", default=CKPT)
|
| 100 |
+
ap.add_argument("--shard-i", type=int, default=0)
|
| 101 |
+
ap.add_argument("--num-shards", type=int, default=1)
|
| 102 |
+
ap.add_argument("--max-input-tokens", type=int, default=4096)
|
| 103 |
+
ap.add_argument("--max-new-tokens", type=int, default=64)
|
| 104 |
+
ap.add_argument("--batch-size", type=int, default=8)
|
| 105 |
+
ap.add_argument("--qnas-per-commit-limit", type=int, default=8)
|
| 106 |
+
ap.add_argument("--repos", nargs="*", default=None)
|
| 107 |
+
args = ap.parse_args()
|
| 108 |
+
|
| 109 |
+
out_dir = Path(args.out_dir)
|
| 110 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 111 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 112 |
+
|
| 113 |
+
rows_by_repo = load_commit_rows_for_gru(Path(COMMITS))
|
| 114 |
+
repos = sorted(rows_by_repo)
|
| 115 |
+
if args.repos:
|
| 116 |
+
repos = [r for r in repos if r in set(args.repos)]
|
| 117 |
+
repos = [r for i, r in enumerate(repos) if i % args.num_shards == args.shard_i]
|
| 118 |
+
repos = [r for r in repos if not (out_dir / f"{safe_name(r)}.pkl").exists()]
|
| 119 |
+
print(f"[shard {args.shard_i}/{args.num_shards}] {len(repos)} repos to do: {repos}", flush=True)
|
| 120 |
+
if not repos:
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
base_model, gru, head, specs, tok = _load_v2_gru_ckpt(
|
| 124 |
+
Path(args.checkpoint), DEFAULT_BASE_MODEL, DEFAULT_TARGET_MODULES, device)
|
| 125 |
+
bos = _get_bos_id(tok)
|
| 126 |
+
meta = commit_metadata(repos)
|
| 127 |
+
|
| 128 |
+
qnas_by_key: Dict[tuple, List[dict]] = defaultdict(list)
|
| 129 |
+
for q in load_qna_rows(Path(QNAS), repo_ids=repos):
|
| 130 |
+
qnas_by_key[(q.repo_id, q.commit_sha)].append(q)
|
| 131 |
+
|
| 132 |
+
def gen(ids_list: List[List[int]]) -> List[str]:
|
| 133 |
+
out: List[str] = []
|
| 134 |
+
for i in range(0, len(ids_list), args.batch_size):
|
| 135 |
+
out += _generate_batch(base_model, tok, device, ids_list[i:i + args.batch_size],
|
| 136 |
+
max_new_tokens=args.max_new_tokens)
|
| 137 |
+
return out
|
| 138 |
+
|
| 139 |
+
for repo in repos:
|
| 140 |
+
t0 = time.time()
|
| 141 |
+
rows = rows_by_repo[repo]
|
| 142 |
+
h = gru.init_hidden(torch.from_numpy(rows[0].repo_state_embedding).to(device).unsqueeze(0))
|
| 143 |
+
ctxs = []
|
| 144 |
+
for row in rows:
|
| 145 |
+
h = gru.step(torch.from_numpy(row.diff_embedding).to(device).unsqueeze(0), h)
|
| 146 |
+
ctxs.append(gru.output_norm(h[-1]).float())
|
| 147 |
+
ctx = torch.cat(ctxs) # [T, 2048]
|
| 148 |
+
|
| 149 |
+
commits, qnas = [], []
|
| 150 |
+
for k, row in enumerate(rows):
|
| 151 |
+
m = meta[repo].get(row.commit_sha, {})
|
| 152 |
+
commits.append({"sha": row.commit_sha, "commit_index": int(row.commit_index),
|
| 153 |
+
"in_repo_split": row.in_repo_split, **m})
|
| 154 |
+
for q in qnas_by_key.get((repo, row.commit_sha), [])[: args.qnas_per_commit_limit]:
|
| 155 |
+
ids = _prepare_prefix_ids(tok, q.prefix, args.max_input_tokens, bos)
|
| 156 |
+
had_bos = bos is not None and ids[0] == bos
|
| 157 |
+
qnas.append({
|
| 158 |
+
"k": k, "test_file": q.test_file, "lineno": int(q.lineno or 0),
|
| 159 |
+
"test_function": q.test_function or "",
|
| 160 |
+
"prompt": tok.decode(ids[1:] if had_bos else ids),
|
| 161 |
+
"had_bos": had_bos, "n_prompt_tokens": len(ids),
|
| 162 |
+
"target": q.target, "_ids": ids,
|
| 163 |
+
})
|
| 164 |
+
|
| 165 |
+
clear_all_lora_weights(base_model, specs)
|
| 166 |
+
for q, p in zip(qnas, gen([q["_ids"] for q in qnas])):
|
| 167 |
+
q["base_pred"] = p
|
| 168 |
+
q.update({f"base_{k}": v for k, v in score(p, q["target"]).items()})
|
| 169 |
+
|
| 170 |
+
by_k: Dict[int, List[dict]] = defaultdict(list)
|
| 171 |
+
for q in qnas:
|
| 172 |
+
by_k[q["k"]].append(q)
|
| 173 |
+
for k, group in by_k.items():
|
| 174 |
+
inject_lora_weights(base_model, specs, head(ctx[k:k + 1]), batch_index=0)
|
| 175 |
+
for q, p in zip(group, gen([q["_ids"] for q in group])):
|
| 176 |
+
q["c2l_pred"] = p
|
| 177 |
+
q.update({f"c2l_{kk}": v for kk, v in score(p, q["target"]).items()})
|
| 178 |
+
|
| 179 |
+
wins = [q for q in qnas if q["c2l_em"] == 1.0 and q["base_em"] == 0.0 and q["k"] > 0]
|
| 180 |
+
by_prev: Dict[int, List[dict]] = defaultdict(list)
|
| 181 |
+
for q in wins:
|
| 182 |
+
by_prev[q["k"] - 1].append(q)
|
| 183 |
+
for k_prev, group in by_prev.items():
|
| 184 |
+
inject_lora_weights(base_model, specs, head(ctx[k_prev:k_prev + 1]), batch_index=0)
|
| 185 |
+
for q, p in zip(group, gen([q["_ids"] for q in group])):
|
| 186 |
+
q["prev_pred"] = p
|
| 187 |
+
q.update({f"prev_{kk}": v for kk, v in score(p, q["target"]).items()})
|
| 188 |
+
clear_all_lora_weights(base_model, specs)
|
| 189 |
+
|
| 190 |
+
for q in qnas:
|
| 191 |
+
q.pop("_ids")
|
| 192 |
+
payload = {"repo_id": repo, "ctx": ctx.cpu().numpy().astype(np.float32),
|
| 193 |
+
"commits": commits, "qnas": qnas,
|
| 194 |
+
"max_input_tokens": args.max_input_tokens,
|
| 195 |
+
"max_new_tokens": args.max_new_tokens}
|
| 196 |
+
tmp = out_dir / f"{safe_name(repo)}.pkl.tmp"
|
| 197 |
+
with open(tmp, "wb") as f:
|
| 198 |
+
pickle.dump(payload, f)
|
| 199 |
+
tmp.replace(out_dir / f"{safe_name(repo)}.pkl")
|
| 200 |
+
n = max(len(qnas), 1)
|
| 201 |
+
print(f"[{repo}] {len(rows)} commits, {len(qnas)} qnas, "
|
| 202 |
+
f"base EM={sum(q['base_em'] for q in qnas)/n:.3f} "
|
| 203 |
+
f"c2l EM={sum(q['c2l_em'] for q in qnas)/n:.3f} "
|
| 204 |
+
f"time-travel={sum(1 for q in wins if q.get('prev_em') == 0.0)} "
|
| 205 |
+
f"({(time.time()-t0)/60:.1f} min)", flush=True)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
main()
|
tools/precompute.sbatch
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Precompute the Code2LoRA timeline Space cache (cr_test, all 51 repos).
|
| 3 |
+
# sbatch --array=0-7 hf_space_demo/tools/precompute.sbatch
|
| 4 |
+
#SBATCH --job-name=c2l_space_cache
|
| 5 |
+
#SBATCH --output=slurm_logs/c2l_space_cache_%A_%a.out
|
| 6 |
+
#SBATCH --error=slurm_logs/c2l_space_cache_%A_%a.err
|
| 7 |
+
#SBATCH --time=03:00:00
|
| 8 |
+
#SBATCH --gres=gpu:h100:1
|
| 9 |
+
#SBATCH --cpus-per-task=8
|
| 10 |
+
#SBATCH --mem=80G
|
| 11 |
+
#SBATCH --account=def-yuntian_gpu
|
| 12 |
+
#SBATCH --export=ALL
|
| 13 |
+
|
| 14 |
+
set -euo pipefail
|
| 15 |
+
module purge 2>/dev/null || true
|
| 16 |
+
module load StdEnv/2023 python/3.12 gcc/12.3 cuda/12.6 arrow
|
| 17 |
+
source /scratch/lhotsko/venvs/qwen-cu126-py312/bin/activate
|
| 18 |
+
cd /home/lhotsko/RepoPeftData
|
| 19 |
+
mkdir -p slurm_logs
|
| 20 |
+
|
| 21 |
+
export PYTHONUNBUFFERED=1
|
| 22 |
+
export HF_HOME="${HF_HOME:-$HOME/.cache/huggingface}"
|
| 23 |
+
export HF_HUB_OFFLINE=1
|
| 24 |
+
|
| 25 |
+
python hf_space_demo/tools/precompute.py \
|
| 26 |
+
--out-dir /home/lhotsko/projects/def-yuntian/lhotsko/c2l_space/cache \
|
| 27 |
+
--shard-i "${SLURM_ARRAY_TASK_ID:-0}" --num-shards 8
|
tools/smoke_test.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""CPU smoke test for the Space.
|
| 3 |
+
|
| 4 |
+
Builds a one-repo data directory (real GRU contexts and prompts, placeholder
|
| 5 |
+
cached predictions), then drives every app handler and serves the UI once.
|
| 6 |
+
|
| 7 |
+
C2L_DATA_DIR=/tmp/c2l_smoke python tools/smoke_test.py --repo MiniMax-AI/Mini-Agent
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import gzip
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import time
|
| 18 |
+
import urllib.request
|
| 19 |
+
from collections import defaultdict
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import pyarrow.parquet as pq
|
| 24 |
+
import torch
|
| 25 |
+
|
| 26 |
+
HERE = Path(__file__).resolve().parents[1]
|
| 27 |
+
sys.path.insert(0, str(HERE))
|
| 28 |
+
|
| 29 |
+
from c2l.core import CommitGRU # noqa: E402
|
| 30 |
+
|
| 31 |
+
COMMITS = "/scratch/lhotsko/REPO_DATASET/commit_parquet_hf_v2/commits/cr_test.parquet"
|
| 32 |
+
QNAS = "/scratch/lhotsko/REPO_DATASET/code2lora_snapshots_hf/qna/cr_test.parquet"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def build(repo: str, out: Path) -> None:
|
| 36 |
+
from huggingface_hub import hf_hub_download
|
| 37 |
+
from transformers import AutoTokenizer
|
| 38 |
+
|
| 39 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 40 |
+
safe = repo.replace("/", "__")
|
| 41 |
+
ct = pq.read_table(COMMITS, filters=[("repo_id", "=", repo)]).to_pylist()
|
| 42 |
+
ct.sort(key=lambda r: r["commit_index"])
|
| 43 |
+
qt = pq.read_table(QNAS, columns=["repo_id", "commit_sha", "test_file", "lineno", "test_function",
|
| 44 |
+
"prefix", "target"], filters=[("repo_id", "=", repo)]).to_pylist()
|
| 45 |
+
by_sha = defaultdict(list)
|
| 46 |
+
for q in qt:
|
| 47 |
+
by_sha[q["commit_sha"]].append(q)
|
| 48 |
+
|
| 49 |
+
state = torch.load(hf_hub_download("code2lora/code2lora-gru", "code2lora_gru.pt"),
|
| 50 |
+
map_location="cpu", weights_only=False, mmap=True)
|
| 51 |
+
gc = state.get("gru_config") or {}
|
| 52 |
+
gru = CommitGRU(int(gc.get("diff_input_dim", 2048)), int(gc.get("repo_state_dim", 2048)),
|
| 53 |
+
int(gc.get("hidden_dim", 2048)))
|
| 54 |
+
gru.load_state_dict(state["gru_state"])
|
| 55 |
+
gru.eval()
|
| 56 |
+
with torch.no_grad():
|
| 57 |
+
h = gru.init_hidden(torch.tensor(ct[0]["repo_state_embedding"]).unsqueeze(0))
|
| 58 |
+
ctx = []
|
| 59 |
+
for r in ct:
|
| 60 |
+
h = gru.step(torch.tensor(r["diff_embedding"]).unsqueeze(0), h)
|
| 61 |
+
ctx.append(gru.context(h).float())
|
| 62 |
+
np.save(out / f"{safe}.ctx.npy", torch.cat(ctx).numpy().astype(np.float32))
|
| 63 |
+
|
| 64 |
+
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-Coder-1.5B")
|
| 65 |
+
bos = tok.bos_token_id if tok.bos_token_id is not None else tok.eos_token_id
|
| 66 |
+
commits, qnas = [], []
|
| 67 |
+
for k, r in enumerate(ct):
|
| 68 |
+
group = []
|
| 69 |
+
for j, q in enumerate(by_sha[r["commit_sha"]][:8]):
|
| 70 |
+
ids = ([bos] + tok.encode(q["prefix"], add_special_tokens=False))[-4096:]
|
| 71 |
+
had_bos = ids[0] == bos
|
| 72 |
+
group.append({"k": k, "test_file": q["test_file"], "lineno": int(q["lineno"] or 0),
|
| 73 |
+
"test_function": q["test_function"] or "",
|
| 74 |
+
"prompt": tok.decode(ids[1:] if had_bos else ids), "had_bos": had_bos,
|
| 75 |
+
"n_prompt_tokens": len(ids), "target": q["target"],
|
| 76 |
+
"base_pred": "placeholder", "base_em": 0.0, "base_es": 0.0,
|
| 77 |
+
"c2l_pred": q["target"], "c2l_em": 1.0, "c2l_es": 1.0,
|
| 78 |
+
**({"prev_pred": "placeholder", "prev_em": 0.0, "prev_es": 0.0} if j == 0 and k else {})})
|
| 79 |
+
qnas.append(group)
|
| 80 |
+
commits.append({"sha": r["commit_sha"], "commit_index": r["commit_index"],
|
| 81 |
+
"timestamp": r["commit_timestamp"], "n_new_assertions": r["n_new_assertions"],
|
| 82 |
+
"diff_preview": (r["production_code_diff"] or "")[:6000],
|
| 83 |
+
"n_qnas": len(group), "base_em": 0.0 if group else None,
|
| 84 |
+
"c2l_em": 1.0 if group else None, "time_travel": bool(group and k)})
|
| 85 |
+
with gzip.open(out / f"{safe}.json.gz", "wt", encoding="utf-8") as f:
|
| 86 |
+
json.dump({"repo_id": repo, "commits": commits, "qnas": qnas,
|
| 87 |
+
"max_input_tokens": 4096, "max_new_tokens": 64}, f)
|
| 88 |
+
(out / "index.json").write_text(json.dumps([{
|
| 89 |
+
"repo_id": repo, "safe": safe, "n_commits": len(commits), "n_qnas": sum(map(len, qnas)),
|
| 90 |
+
"base_em": 0.0, "c2l_em": 1.0, "n_time_travel": sum(c["time_travel"] for c in commits),
|
| 91 |
+
"first_date": commits[0]["timestamp"], "last_date": commits[-1]["timestamp"],
|
| 92 |
+
"description": "smoke test", "stars": 1234}]))
|
| 93 |
+
print(f"[build] {repo}: {len(commits)} commits, {sum(map(len, qnas))} qnas", flush=True)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def drive() -> None:
|
| 97 |
+
t0 = time.time()
|
| 98 |
+
import app
|
| 99 |
+
print(f"[app] import + engine {time.time() - t0:.0f}s", flush=True)
|
| 100 |
+
safe = app.INDEX[0]["safe"]
|
| 101 |
+
r = app.on_repo(safe)
|
| 102 |
+
k1 = r[1].value if hasattr(r[1], "value") else r[1]["value"]
|
| 103 |
+
print("[on_repo] ok, start commit", k1, flush=True)
|
| 104 |
+
r = app.on_commit(safe, k1, None)
|
| 105 |
+
i = r[3].value if hasattr(r[3], "value") else r[3]["value"]
|
| 106 |
+
print("[on_commit] ok, pick", i, flush=True)
|
| 107 |
+
t = time.time()
|
| 108 |
+
status, st, *_ = app.on_adapt(safe, k1, i)
|
| 109 |
+
print(f"[on_adapt] {time.time() - t:.2f}s state={st}", flush=True)
|
| 110 |
+
print(status[:400])
|
| 111 |
+
_, preds, _ = app.on_pick(safe, k1, i, st)
|
| 112 |
+
assert "Code2LoRA @ commit" in preds, preds
|
| 113 |
+
t = time.time()
|
| 114 |
+
live = app.on_live(safe, k1, i, st, progress=lambda *a, **k: None)
|
| 115 |
+
print(f"[on_live] {time.time() - t:.1f}s\n{live}", flush=True)
|
| 116 |
+
t = time.time()
|
| 117 |
+
prompt = app.on_prefill(safe, k1, i)
|
| 118 |
+
custom = app.on_custom(safe, k1, prompt, 32, st, progress=lambda *a, **k: None)
|
| 119 |
+
print(f"[on_custom] {time.time() - t:.1f}s\n{custom[:1500]}", flush=True)
|
| 120 |
+
for k in range(1, len(app.load_repo(safe)["commits"]) + 1):
|
| 121 |
+
app.on_commit(safe, k, st)
|
| 122 |
+
app.on_adapt(safe, k, 0)
|
| 123 |
+
print("[all commits] ok", flush=True)
|
| 124 |
+
|
| 125 |
+
app.demo.queue(max_size=32).launch(server_port=7861, prevent_thread_lock=True)
|
| 126 |
+
body = urllib.request.urlopen("http://127.0.0.1:7861/", timeout=30).read()
|
| 127 |
+
print(f"[serve] GET / -> {len(body)} bytes", flush=True)
|
| 128 |
+
app.demo.close()
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
ap = argparse.ArgumentParser()
|
| 133 |
+
ap.add_argument("--repo", default="MiniMax-AI/Mini-Agent")
|
| 134 |
+
ap.add_argument("--build-only", action="store_true")
|
| 135 |
+
args = ap.parse_args()
|
| 136 |
+
data = Path(os.environ["C2L_DATA_DIR"])
|
| 137 |
+
if not (data / "index.json").exists():
|
| 138 |
+
build(args.repo, data)
|
| 139 |
+
if not args.build_only:
|
| 140 |
+
drive()
|
tools/smoke_test.sbatch
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# CPU smoke test mirroring the free Space tier (2 vCPU).
|
| 3 |
+
# sbatch hf_space_demo/tools/smoke_test.sbatch
|
| 4 |
+
#SBATCH --job-name=c2l_space_smoke
|
| 5 |
+
#SBATCH --output=slurm_logs/c2l_space_smoke_%j.out
|
| 6 |
+
#SBATCH --time=01:00:00
|
| 7 |
+
#SBATCH --cpus-per-task=2
|
| 8 |
+
#SBATCH --mem=24G
|
| 9 |
+
#SBATCH --account=def-yuntian
|
| 10 |
+
|
| 11 |
+
set -euo pipefail
|
| 12 |
+
module purge 2>/dev/null || true
|
| 13 |
+
module load StdEnv/2023 python/3.12 arrow
|
| 14 |
+
P=/home/lhotsko/projects/def-yuntian/lhotsko/c2l_space
|
| 15 |
+
source $P/venv/bin/activate
|
| 16 |
+
cd /home/lhotsko/RepoPeftData/hf_space_demo
|
| 17 |
+
|
| 18 |
+
export PYTHONUNBUFFERED=1 HF_HOME=$P/hf HF_HUB_OFFLINE=1 C2L_DATA_DIR=$P/smoke_data
|
| 19 |
+
export OMP_NUM_THREADS=2 GRADIO_ANALYTICS_ENABLED=False
|
| 20 |
+
python tools/smoke_test.py
|
upload_checkpoint.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Upload a trained Code2LoRA-GRU checkpoint to a Hugging Face model repo.
|
| 3 |
-
|
| 4 |
-
Usage:
|
| 5 |
-
huggingface-cli login # once, to set your token
|
| 6 |
-
python upload_checkpoint.py \
|
| 7 |
-
--ckpt /path/to/gru_head.best.pt \
|
| 8 |
-
--repo your-name/code2lora-gru
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import argparse
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def main() -> None:
|
| 18 |
-
ap = argparse.ArgumentParser(description=__doc__)
|
| 19 |
-
ap.add_argument("--ckpt", required=True, help="Path to gru_head.*.pt")
|
| 20 |
-
ap.add_argument("--repo", required=True,
|
| 21 |
-
help="Target HF model repo id, e.g. your-name/code2lora-gru")
|
| 22 |
-
ap.add_argument("--filename", default="gru_head.best.pt",
|
| 23 |
-
help="Name to store it under in the repo.")
|
| 24 |
-
ap.add_argument("--private", action="store_true")
|
| 25 |
-
args = ap.parse_args()
|
| 26 |
-
|
| 27 |
-
ckpt = Path(args.ckpt)
|
| 28 |
-
if not ckpt.exists():
|
| 29 |
-
raise SystemExit(f"Checkpoint not found: {ckpt}")
|
| 30 |
-
|
| 31 |
-
from huggingface_hub import HfApi
|
| 32 |
-
api = HfApi()
|
| 33 |
-
api.create_repo(args.repo, repo_type="model", private=args.private,
|
| 34 |
-
exist_ok=True)
|
| 35 |
-
print(f"Uploading {ckpt} ({ckpt.stat().st_size / 1e9:.2f} GB) -> "
|
| 36 |
-
f"{args.repo}/{args.filename} ...")
|
| 37 |
-
api.upload_file(
|
| 38 |
-
path_or_fileobj=str(ckpt),
|
| 39 |
-
path_in_repo=args.filename,
|
| 40 |
-
repo_id=args.repo,
|
| 41 |
-
repo_type="model",
|
| 42 |
-
)
|
| 43 |
-
print("Done. Set CODE2LORA_CKPT_REPO=%s in the Space settings." % args.repo)
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
if __name__ == "__main__":
|
| 47 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|