code2lora commited on
Commit
cdd31be
·
verified ·
1 Parent(s): 14dd762

Timeline demo: precomputed GRU states, live adapter generation

Browse files
README.md CHANGED
@@ -1,130 +1,36 @@
1
  ---
2
- title: Code2LoRA-GRU
3
  emoji: 🧬
4
- colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.9.1
8
  app_file: app.py
9
- pinned: false
10
  license: mit
11
- short_description: Turn a repo's commit history into a LoRA adapter, live.
 
 
 
 
 
12
  ---
13
 
14
- # Code2LoRA-GRU — interactive demo
15
 
16
- Paste a Python repository (a **git URL** or a **local clone path**) and watch a
17
- trained hypernetwork turn its *commit history* into a parameter-efficient LoRA
18
- adapter for a frozen code model — then watch that adapter answer questions the
19
- base model gets wrong.
 
20
 
21
- This Space implements the inference pipeline from **Code2LoRA: Hypernetworks
22
- for Repository-Conditioned and Commit-Streaming Adapters of Code Language
23
- Models**.
 
 
 
24
 
25
- ---
26
-
27
- ## What you get
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-GRU — interactive demo (Hugging Face Space).
2
 
3
- Paste a Python repository (local path or git URL). The Space:
4
-
5
- 1. mines its commit history and selects the commits that introduce new test
6
- assertions (the "GRU pass"),
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 tempfile
20
- import traceback
 
 
 
 
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
- # Process-wide engine (lazy model loading happens on first use)
30
  # ---------------------------------------------------------------------------
31
 
32
- ENGINE = Engine()
33
- WORK_DIR = Path(tempfile.gettempdir()) / "code2lora_demo_repos"
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
- HOW_IT_WORKS_MD = """
61
- **Code2LoRA-GRU** turns a repository into a *parameter-efficient adapter* for a
62
- frozen code model (`Qwen2.5-Coder-1.5B`) — no fine-tuning, no in-context
63
- retrieval at inference time.
64
-
65
- 1. **Commit selection** — we keep only commits that introduce *new or changed*
66
- test assertions; these form the streaming sequence.
67
- 2. **Encoding** — each kept commit's production-code diff is embedded with a
68
- frozen `Qwen3-Embedding-0.6B` (2048-d). The initial repository state seeds
69
- the recurrent hidden state.
70
- 3. **Recurrence** — a GRU walks the commits, accumulating repository knowledge
71
- in its hidden state in **O(1) per commit**.
72
- 4. **Adapter generation** — a shared-basis head maps the hidden state to LoRA
73
- weights for every attention/MLP projection, which are injected into the
74
- frozen base model.
75
- 5. **Prediction** — the adapted model fills in a held-out assertion that plain
76
- prompting cannot recover.
77
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
 
80
  # ---------------------------------------------------------------------------
81
  # Rendering helpers
82
  # ---------------------------------------------------------------------------
83
 
84
- def _stat_card(label: str, value: str, sub: str = "", accent: str = "#4f8cff") -> str:
85
- return f"""
86
- <div style="flex:1;min-width:150px;background:#161922;border:1px solid #2a2e3a;
87
- border-radius:12px;padding:14px 16px;">
88
- <div style="font-size:12px;color:#8b93a7;text-transform:uppercase;
89
- letter-spacing:.05em;">{html.escape(label)}</div>
90
- <div style="font-size:28px;font-weight:700;color:{accent};margin-top:4px;">
91
- {html.escape(value)}</div>
92
- <div style="font-size:12px;color:#8b93a7;margin-top:2px;">{html.escape(sub)}</div>
93
- </div>"""
94
-
95
-
96
- def render_stats(session: RepoSession) -> str:
97
- r = session.result
98
- pct_kept = (100.0 * r.kept_commits / r.total_commits) if r.total_commits else 0.0
99
- boundary = session.endpoint_index + 1
100
- cards = "".join([
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
- <div style="background:#0f1117;border:1px solid #2a2e3a;border-radius:12px;
173
- padding:14px 16px;margin-bottom:12px;">
174
- <div style="font-size:12px;color:#8b93a7;text-transform:uppercase;
175
- letter-spacing:.05em;margin-bottom:6px;">
176
- Test prompt (tail · {res['n_prompt_tokens']} tokens) — model must
177
- complete the ▶ blank</div>
178
- <pre style="white-space:pre-wrap;font-family:ui-monospace,Menlo,monospace;
179
- font-size:12.5px;color:#cfd6e4;margin:0;max-height:320px;
180
- overflow:auto;">{html.escape(tail)}<span style="color:#22d3a6;
181
- ">▶ ???</span></pre>
182
- </div>
183
- <div style="display:flex;gap:12px;flex-wrap:wrap;">{cards}</div>"""
184
-
185
-
186
- def _qna_choices(session: RepoSession) -> List:
187
- """Build dropdown choices, preferring held-out (val/test) assertions."""
188
- items = []
189
- for c in session.result.commits:
190
- for qi, q in enumerate(c.qnas):
191
- held = c.in_repo_split in ("val", "test")
192
- items.append((c, qi, q, held))
193
- # Held-out first, then train; cap to keep the dropdown snappy.
194
- items.sort(key=lambda x: (not x[3], x[0].commit_index))
195
- choices = []
196
- for c, qi, q, held in items[:300]:
197
- tag = "★ held-out" if held else "train"
198
- fn = q.test_function or "?"
199
- snippet = " ".join(q.prefix.strip().splitlines()[-1:])[-48:]
200
- label = f"[{tag}] {Path(q.test_file).name}::{fn} L{q.lineno} — …{snippet}"
201
- choices.append((label, f"{c.commit_index}:{qi}"))
202
- return choices
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
 
205
  # ---------------------------------------------------------------------------
206
- # Callbacks
207
  # ---------------------------------------------------------------------------
208
 
209
- def process_repo_cb(repo_input: str, progress=gr.Progress()):
210
- from c2l_demo import viz
211
-
212
- def _cb(msg, frac):
213
- try:
214
- progress(min(max(frac, 0.0), 1.0), desc=msg)
215
- except Exception:
216
- pass
217
-
218
- try:
219
- progress(0.0, desc="Resolving repository…")
220
- repo_dir, repo_id = ENGINE.resolve_repo(repo_input, WORK_DIR, progress=_cb)
221
- session = ENGINE.process_and_walk(repo_dir, repo_id, walk_fraction=0.8,
222
- progress=_cb)
223
- except Exception as e:
224
- err = (f"<div style='color:#ff6b6b;background:#1a1014;border:1px solid "
225
- f"#ff6b6b;border-radius:10px;padding:14px;'><b>Could not process "
226
- f"this repository.</b><br><br><pre style='white-space:pre-wrap;"
227
- f"color:#ffb3b3;font-size:12px;'>{html.escape(str(e))}</pre></div>")
228
- return (err, None, None, None,
229
- gr.update(choices=[], value=None), None, "")
230
-
231
- stats = render_stats(session)
232
- fig_timeline = viz.plot_commit_timeline(session.result, session.endpoint_index)
233
- fig_types = viz.plot_assertion_type_breakdown(session.result)
234
- fig_gru = viz.plot_gru_processing(session)
235
- choices = _qna_choices(session)
236
- dd = gr.update(choices=choices, value=(choices[0][1] if choices else None))
237
- ready = ("<div style='color:#22d3a6;'>Adapter ready — pick a held-out "
238
- "assertion below and run the prediction.</div>")
239
- return stats, fig_timeline, fig_types, fig_gru, dd, session, ready
240
-
241
-
242
- def predict_cb(session: Optional[RepoSession], selected: Optional[str]):
243
- if session is None:
244
- return ("<div style='color:#ff6b6b;'>Process a repository first.</div>")
245
- if not selected:
246
- return ("<div style='color:#ff6b6b;'>Pick an assertion to predict.</div>")
247
- try:
248
- ci_str, qi_str = selected.split(":")
249
- ci, qi = int(ci_str), int(qi_str)
250
- commit = next(c for c in session.result.commits if c.commit_index == ci)
251
- q = commit.qnas[qi]
252
- res = ENGINE.predict(session, q.prefix, q.target)
253
- return render_prediction(res)
254
- except Exception as e:
255
- return (f"<div style='color:#ff6b6b;'><b>Prediction failed.</b><pre "
256
- f"style='white-space:pre-wrap;font-size:12px;'>"
257
- f"{html.escape(traceback.format_exc())}</pre></div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 {background: linear-gradient(135deg,#1b2a4a 0%,#0f1117 60%);
267
- border:1px solid #2a2e3a;border-radius:16px;padding:24px 28px;margin-bottom:8px;}
268
- #hero h1 {font-size:30px;margin:0 0 6px 0;background:linear-gradient(90deg,#4f8cff,#22d3a6);
269
- -webkit-background-clip:text;-webkit-text-fill-color:transparent;}
270
- .section-title {font-size:20px;font-weight:700;margin-top:6px;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  """
272
 
 
 
 
 
 
 
 
 
 
 
273
 
274
- def build_demo():
275
- with gr.Blocks(title="Code2LoRA-GRU Demo", theme=gr.themes.Soft(
276
- primary_hue="blue", neutral_hue="slate"), css=CSS) as demo:
277
- session_state = gr.State(value=None)
 
 
278
 
279
- gr.HTML("""
280
- <div id="hero">
281
- <h1>Code2LoRA-GRU</h1>
282
- <div style="color:#cfd6e4;font-size:15px;max-width:820px;">
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
- with gr.Accordion("How it works", open=False):
291
- gr.Markdown(HOW_IT_WORKS_MD)
292
 
293
- # 0) Instructions / requirements
294
- with gr.Accordion("0 · Repository requirements (read me first)", open=True):
295
- gr.Markdown(REQUIREMENTS_MD)
 
 
 
296
 
297
- # 1) Input
298
- gr.Markdown("## 1 · Point at a repository", elem_classes="section-title")
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
- types_plot = gr.Plot(label="Assertion-type distribution")
316
-
317
- # 2.2) GRU processing
318
- gr.Markdown("## 2.2 · How the GRU processes the repository",
319
- elem_classes="section-title")
320
- gru_plot = gr.Plot(label="Hidden-state trajectory + accumulated repository knowledge")
321
-
322
- # 3) Prediction
323
- gr.Markdown("## 3 · Watch the adapter predict a held-out assertion",
324
- elem_classes="section-title")
325
- gr.Markdown(
326
- "Pick an assertion (★ = a *held-out* val/test commit the adapter "
327
- "never saw). We blank its right-hand side and ask the model to fill "
328
- "it in — first the plain base model, then with the repository "
329
- "adapter injected.")
330
- with gr.Row():
331
- qna_dropdown = gr.Dropdown(label="Held-out assertion to complete",
332
- choices=[], scale=4)
333
- predict_btn = gr.Button("Predict", variant="primary", scale=1)
334
- prediction_html = gr.HTML()
335
-
336
- gr.Markdown(
337
- "<div style='color:#8b93a7;font-size:12px;margin-top:16px;'>"
338
- "Base model: <code>Qwen/Qwen2.5-Coder-1.5B</code> · Encoder: "
339
- "<code>Qwen/Qwen3-Embedding-0.6B</code> · Adapter: Code2LoRA-GRU "
340
- "(rank-16 LoRA generated by a commit-streaming GRU).</div>")
341
-
342
- process_btn.click(
343
- process_repo_cb, inputs=[repo_input],
344
- outputs=[stats_html, timeline_plot, types_plot, gru_plot,
345
- qna_dropdown, session_state, status])
346
- predict_btn.click(
347
- predict_cb, inputs=[session_state, qna_dropdown],
348
- outputs=[prediction_html])
349
- return demo
350
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
  if __name__ == "__main__":
353
- build_demo().queue().launch(server_name="0.0.0.0", server_port=7860)
 
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
- """Evaluation metrics for the demo.
 
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
- gradio>=4.44,<6
2
  torch>=2.3
3
- transformers>=4.45
4
  accelerate>=0.34
5
- huggingface_hub>=0.25
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()