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

Code2LoRA-GRU interactive demo

Browse files
README.md CHANGED
@@ -1,13 +1,130 @@
1
  ---
2
- title: Code2lora Gru Demo
3
- emoji: 🏆
4
- colorFrom: purple
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.17.3
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ```
app.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # ---------------------------------------------------------------------------
261
+ # UI
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)
c2l_demo/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/core.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model core for Code2LoRA-GRU inference.
2
+
3
+ Faithful port of ``hypernetwork/code2lora_core.py`` from the RepoPeftData
4
+ project, trimmed to the pieces needed for inference (the parquet loaders that
5
+ the trainer uses are intentionally omitted so this Space has no pyarrow
6
+ dependency).
7
+
8
+ The two networks that get loaded from a ``gru_head.*.pt`` checkpoint are:
9
+
10
+ * :class:`CommitGRU` -- a 1-layer GRU that ingests per-commit diff
11
+ embeddings; ``h_0`` comes from a learnable projection of the initial
12
+ repo-state embedding.
13
+ * :class:`Code2LoRAHead` -- maps the GRU hidden state to one LoRA (A, B)
14
+ pair per module *type* (q_proj, v_proj, ...), shared across all
15
+ transformer layers.
16
+
17
+ The generated weights are pushed into :class:`LoRA` wrappers that replace the
18
+ target ``nn.Linear`` modules of the frozen base LLM.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import math
24
+ import re
25
+ from dataclasses import dataclass
26
+ from typing import Any, Dict, List, Optional, Tuple
27
+
28
+ import torch
29
+ import torch.nn as nn
30
+ import torch.nn.functional as F
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # LoRA module + injection
35
+ # ---------------------------------------------------------------------------
36
+
37
+ class LoRA(nn.Module):
38
+ """Wraps an ``nn.Linear`` with an additive low-rank update.
39
+
40
+ Forward: ``y = base(x) + scaling * (x @ A^T) @ B^T`` where the per-call
41
+ A: ``[rank, in_features]`` and B: ``[out_features, rank]`` come from the
42
+ hypernet via :meth:`set_lora_weights`.
43
+ """
44
+
45
+ def __init__(self, base: nn.Linear, in_features: int, out_features: int,
46
+ rank: int, alpha: float):
47
+ super().__init__()
48
+ self.base = base
49
+ for p in self.base.parameters():
50
+ p.requires_grad = False
51
+ self.in_features = in_features
52
+ self.out_features = out_features
53
+ self.rank = rank
54
+ self.scaling = float(alpha) / float(max(1, rank))
55
+ self.A: Optional[torch.Tensor] = None # [rank, in_features]
56
+ self.B: Optional[torch.Tensor] = None # [out_features, rank]
57
+
58
+ def set_lora_weights(self, A: torch.Tensor, B: torch.Tensor) -> None:
59
+ self.A = A
60
+ self.B = B
61
+
62
+ def clear_lora_weights(self) -> None:
63
+ self.A = None
64
+ self.B = None
65
+
66
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
67
+ y = self.base(x)
68
+ if self.A is None or self.B is None:
69
+ return y
70
+ x_f32 = x.detach().to(torch.float32)
71
+ A = self.A.to(torch.float32)
72
+ B = self.B.to(torch.float32)
73
+ delta = F.linear(F.linear(x_f32, A), B) * self.scaling
74
+ return y + delta.to(dtype=y.dtype)
75
+
76
+
77
+ @dataclass
78
+ class ModuleSpec:
79
+ full_name: str # e.g. 'model.layers.5.self_attn.q_proj'
80
+ layer_idx: int
81
+ type: str # e.g. 'q_proj'
82
+ in_features: int
83
+ out_features: int
84
+
85
+
86
+ def get_module_specs(model: nn.Module, target_module_types: List[str]
87
+ ) -> List[ModuleSpec]:
88
+ """Discover every nn.Linear whose name contains a target type."""
89
+ specs: List[ModuleSpec] = []
90
+ pat = re.compile(r"\bmodel\.layers\.(\d+)\.")
91
+ for name, m in model.named_modules():
92
+ match_type = next((t for t in target_module_types if t in name), None)
93
+ if match_type is None:
94
+ continue
95
+ if not isinstance(m, nn.Linear):
96
+ continue
97
+ m_layer = pat.search(name)
98
+ layer_idx = int(m_layer.group(1)) if m_layer else -1
99
+ specs.append(ModuleSpec(
100
+ full_name=name,
101
+ layer_idx=layer_idx,
102
+ type=match_type,
103
+ in_features=int(m.in_features),
104
+ out_features=int(m.out_features),
105
+ ))
106
+ specs.sort(key=lambda s: (s.layer_idx, s.full_name))
107
+ return specs
108
+
109
+
110
+ def replace_with_lora(model: nn.Module, specs: List[ModuleSpec],
111
+ rank: int, alpha: float) -> None:
112
+ """Replace each target ``nn.Linear`` with a :class:`LoRA` wrapper. Idempotent."""
113
+ named = dict(model.named_modules())
114
+ device = next(model.parameters()).device
115
+ dtype = next(model.parameters()).dtype
116
+ for sp in specs:
117
+ parent_name, attr = sp.full_name.rsplit(".", 1)
118
+ orig = getattr(named[parent_name], attr)
119
+ if isinstance(orig, LoRA):
120
+ continue
121
+ assert isinstance(orig, nn.Linear), \
122
+ f"{sp.full_name} is not nn.Linear (got {type(orig)})"
123
+ wrapped = LoRA(orig, sp.in_features, sp.out_features,
124
+ rank, alpha).to(device=device, dtype=dtype)
125
+ setattr(named[parent_name], attr, wrapped)
126
+
127
+
128
+ def inject_lora_weights(model: nn.Module, specs: List[ModuleSpec],
129
+ head_out: Dict[str, Dict[str, torch.Tensor]],
130
+ batch_index: int = 0) -> None:
131
+ """Push ``head_out['A'][type]`` / ``head_out['B'][type]`` into each LoRA."""
132
+ A_by_type = head_out["A"]
133
+ B_by_type = head_out["B"]
134
+ named = dict(model.named_modules())
135
+ for sp in specs:
136
+ named[sp.full_name].set_lora_weights(
137
+ A_by_type[sp.type][batch_index],
138
+ B_by_type[sp.type][batch_index],
139
+ )
140
+
141
+
142
+ def clear_all_lora_weights(model: nn.Module, specs: List[ModuleSpec]) -> None:
143
+ """Reset every wrapper LoRA to identity (base model only)."""
144
+ named = dict(model.named_modules())
145
+ for sp in specs:
146
+ mod = named.get(sp.full_name)
147
+ if isinstance(mod, LoRA):
148
+ mod.clear_lora_weights()
149
+
150
+
151
+ def discover_module_types_and_dims(specs: List[ModuleSpec]
152
+ ) -> Dict[str, Tuple[int, int]]:
153
+ type_dims: Dict[str, Tuple[int, int]] = {}
154
+ for sp in specs:
155
+ if sp.type in type_dims:
156
+ assert type_dims[sp.type] == (sp.in_features, sp.out_features), \
157
+ f"type {sp.type} appears with inconsistent dims"
158
+ continue
159
+ type_dims[sp.type] = (sp.in_features, sp.out_features)
160
+ return type_dims
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Shared LoRA generation head
165
+ # ---------------------------------------------------------------------------
166
+
167
+ class Code2LoRAHead(nn.Module):
168
+ """Maps a context vector to one LoRA (A, B) pair per module type."""
169
+
170
+ def __init__(
171
+ self,
172
+ input_dim: int,
173
+ type_dims: Dict[str, Tuple[int, int]],
174
+ hidden_dim: int = 1024,
175
+ rank: int = 16,
176
+ init_log_scale: float = -3.5,
177
+ ):
178
+ super().__init__()
179
+ self.input_dim = input_dim
180
+ self.hidden_dim = hidden_dim
181
+ self.rank = rank
182
+ self.type_dims = dict(type_dims)
183
+ self.types = sorted(type_dims.keys())
184
+
185
+ self.trunk = nn.Sequential(
186
+ nn.Linear(input_dim, hidden_dim),
187
+ nn.GELU(),
188
+ nn.Linear(hidden_dim, hidden_dim),
189
+ nn.GELU(),
190
+ )
191
+ self.heads_A = nn.ModuleDict({
192
+ t: nn.Linear(hidden_dim, rank * type_dims[t][0]) for t in self.types
193
+ })
194
+ self.heads_B = nn.ModuleDict({
195
+ t: nn.Linear(hidden_dim, type_dims[t][1] * rank) for t in self.types
196
+ })
197
+ self.log_scale_A = nn.ParameterDict({
198
+ t: nn.Parameter(torch.tensor(init_log_scale)) for t in self.types
199
+ })
200
+ self.log_scale_B = nn.ParameterDict({
201
+ t: nn.Parameter(torch.tensor(init_log_scale)) for t in self.types
202
+ })
203
+
204
+ def forward(self, ctx: torch.Tensor) -> Dict[str, Dict[str, torch.Tensor]]:
205
+ if ctx.dim() == 3:
206
+ ctx = torch.max(ctx, dim=1).values
207
+ h = self.trunk(ctx.float())
208
+ h = F.normalize(h, p=2, dim=-1) * math.sqrt(self.hidden_dim)
209
+
210
+ A_out: Dict[str, torch.Tensor] = {}
211
+ B_out: Dict[str, torch.Tensor] = {}
212
+ for t in self.types:
213
+ in_f, out_f = self.type_dims[t]
214
+ A_raw = self.heads_A[t](h).view(-1, self.rank, in_f)
215
+ B_raw = self.heads_B[t](h).view(-1, out_f, self.rank)
216
+ scale_A = torch.exp(self.log_scale_A[t]).clamp(1e-5, 0.3)
217
+ scale_B = torch.exp(self.log_scale_B[t]).clamp(1e-5, 0.3)
218
+ A_out[t] = torch.tanh(A_raw) * scale_A
219
+ B_out[t] = torch.tanh(B_raw) * scale_B
220
+ return {"A": A_out, "B": B_out}
221
+
222
+ def config_dict(self) -> Dict[str, Any]:
223
+ return {
224
+ "input_dim": self.input_dim,
225
+ "hidden_dim": self.hidden_dim,
226
+ "rank": self.rank,
227
+ "types": self.types,
228
+ "type_dims": {t: list(v) for t, v in self.type_dims.items()},
229
+ }
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Commit-streaming GRU
234
+ # ---------------------------------------------------------------------------
235
+
236
+ class CommitGRU(nn.Module):
237
+ """One-layer GRU that ingests per-commit diff embeddings.
238
+
239
+ ``h_0`` is a learnable projection of the initial repo-state embedding;
240
+ ``h_T`` is fed into :class:`Code2LoRAHead`.
241
+ """
242
+
243
+ def __init__(
244
+ self,
245
+ diff_input_dim: int = 2048,
246
+ repo_state_dim: int = 2048,
247
+ hidden_dim: int = 2048,
248
+ ):
249
+ super().__init__()
250
+ self.diff_input_dim = diff_input_dim
251
+ self.repo_state_dim = repo_state_dim
252
+ self.hidden_dim = hidden_dim
253
+
254
+ self.diff_proj = nn.Sequential(
255
+ nn.Linear(diff_input_dim, hidden_dim),
256
+ nn.LayerNorm(hidden_dim),
257
+ )
258
+ self.repo_init_proj = nn.Sequential(
259
+ nn.Linear(repo_state_dim, hidden_dim),
260
+ nn.GELU(),
261
+ nn.LayerNorm(hidden_dim),
262
+ )
263
+ self.gru = nn.GRU(
264
+ input_size=hidden_dim,
265
+ hidden_size=hidden_dim,
266
+ num_layers=1,
267
+ batch_first=True,
268
+ )
269
+ self.output_norm = nn.LayerNorm(hidden_dim)
270
+
271
+ def init_hidden(self, repo_state_emb_0: torch.Tensor) -> torch.Tensor:
272
+ h0 = self.repo_init_proj(repo_state_emb_0.float())
273
+ return h0.unsqueeze(0)
274
+
275
+ def step(self, diff_emb: torch.Tensor, h_prev: torch.Tensor) -> torch.Tensor:
276
+ x = self.diff_proj(diff_emb.float()).unsqueeze(1) # [B, 1, H]
277
+ _, h_new = self.gru(x, h_prev)
278
+ return h_new
279
+
280
+ def context(self, h: torch.Tensor) -> torch.Tensor:
281
+ """Match the eval path: ctx = output_norm(h[-1])."""
282
+ return self.output_norm(h[-1])
283
+
284
+
285
+ __all__ = [
286
+ "LoRA",
287
+ "ModuleSpec",
288
+ "get_module_specs",
289
+ "replace_with_lora",
290
+ "inject_lora_weights",
291
+ "clear_all_lora_weights",
292
+ "discover_module_types_and_dims",
293
+ "Code2LoRAHead",
294
+ "CommitGRU",
295
+ ]
c2l_demo/embedding.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/metrics.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
24
+ s = s.replace(tok, "")
25
+ return s.strip()
26
+
27
+
28
+ def strip_comments(s: str) -> str:
29
+ return s.split("#")[0].strip()
30
+
31
+
32
+ def normalize_for_match(s: str) -> str:
33
+ s = s.strip().rstrip(":, \t")
34
+ s = s.replace(", ", ",").replace(" ,", ",")
35
+ return " ".join(s.split())
36
+
37
+
38
+ def exact_match(pred: str, ref: str) -> bool:
39
+ return normalize_for_match(pred) == normalize_for_match(ref)
40
+
41
+
42
+ 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:
85
+ pred = pred.split("\n")[0]
86
+ pred = strip_comments(pred)
87
+ ref = strip_comments(target).strip()
88
+ if ref:
89
+ if "," not in ref and "," in pred:
90
+ pred = pred.split(",")[0].strip()
91
+ if len(ref.split()) == 1 and len(pred.strip().split()) > 1:
92
+ pred = pred.strip().split()[0]
93
+ return pred.strip()
94
+
95
+
96
+ 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
+ ]
c2l_demo/viz.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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
upload_checkpoint.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()