Skip to content

Commit 19c869d

Browse files
jmjavacursoragent
andauthored
Add an offline harder multi-file refactor corpus before Halo. (#14)
Phase 3 still lacked behavior-preservation cases that span files and distinguish leftover aliases from changed semantics. Score those fixtures in CI; skip live Ollama when the server is down. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 822aaaf commit 19c869d

15 files changed

Lines changed: 1059 additions & 13 deletions

‎.github/workflows/tests.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
PYTHONPATH: src
2121
- name: Fixture corpus
2222
run: python scripts/run_eval.py
23+
- name: Harder multi-file fixtures
24+
run: python scripts/prove_multifile_refactor.py
2325
- name: Scripted orchestrator
2426
run: python scripts/run_orchestration.py
2527
- name: Stub MCP golden

‎README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ connected through SSH local forwarding.
7979
```bash
8080
PYTHONPATH=src .venv/bin/python -m unittest discover -s tests -v
8181
.venv/bin/python scripts/run_eval.py
82+
.venv/bin/python scripts/prove_multifile_refactor.py
8283
.venv/bin/python scripts/run_harness.py --backend stub --profile observed --out eval-runs/observed
8384
.venv/bin/python scripts/run_orchestration.py
8485
.venv/bin/python scripts/run_harness.py --backend stub --profile golden --orchestrate
@@ -108,6 +109,15 @@ connected through SSH local forwarding.
108109
.venv/bin/python scripts/prove_refactor_acceptance.py --model strong
109110
```
110111

112+
Harder multi-file cases (exception rename, dataclass field rename, tuple
113+
return + facade) are offline by default. `--live` skips with exit 0 when
114+
Ollama is down; that skip is not a quality pass:
115+
116+
```bash
117+
.venv/bin/python scripts/prove_multifile_refactor.py
118+
.venv/bin/python scripts/prove_multifile_refactor.py --live --model fast
119+
```
120+
111121
The unit suite proves deterministic client, safety, evaluation-scorer,
112122
and harness behavior. The live acceptance scripts are the evidence that the
113123
stdio MCP server can reach the configured Ollama runtime and produce usable

‎docs/evaluation-protocol.md‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ The same scorer runs against:
6060
| `move_function_imports` | `local_refactor` | Move `clamp` into `bounds.py` and update `report.py` imports |
6161
| `extract_shared_parser` | `local_refactor` | Extract `parse_fields` into `csv_parse.py`; both callers import it |
6262
| `split_pipeline` | `local_refactor` | Split `run` into `load.py` / `transform.py` / `pipeline.py` |
63+
| `rename_exception_across_files` | `local_refactor` | Rename `QuotaError` → `LimitError` across raise and catch; no alias |
64+
| `rename_dataclass_field` | `local_refactor` | Rename `Person.years` → `age` in producer and consumer; no property alias |
65+
| `widen_return_keep_facade` | `local_refactor` | `apply_discount` returns `(discounted, saved)`; `line_total` / `savings` stay ints |
6366
| `implement_clamp` | `local_code` | Implement `clamp` from a spec, no starter file |
6467
| `explain_clamp` | `local_explain` | Prose: names the function and bounds; mentions clipping |
6568
| `review_login` | `local_review` | Prose first-pass: flags None and missing auth. **Not** an apply |
@@ -73,6 +76,9 @@ tell layers apart:
7376
- one of two files → format fail (missing required path)
7477
- `ERROR:` payload → transport fail
7578
- unified diff only → format pass, structure skipped
79+
- leftover `QuotaError` alias or `years` property → structure fail
80+
- exception threshold or greeting format drift → behavior fail
81+
- `apply_discount` still returns an int → structure fail; wrong `DISCOUNT_PERCENT` → behavior fail
7682

7783
## Measurement harness
7884

@@ -170,16 +176,21 @@ Fixture protocol (no GPU; this is what Cloud Agents can run):
170176
```bash
171177
PYTHONPATH=src .venv/bin/python -m unittest discover -s tests -v
172178
.venv/bin/python scripts/run_eval.py
179+
.venv/bin/python scripts/run_eval.py --suite harder
180+
.venv/bin/python scripts/prove_multifile_refactor.py
173181
.venv/bin/python scripts/run_orchestration.py
174182
.venv/bin/python scripts/run_harness.py --backend stub --profile golden --orchestrate --fast-ms 1 --strong-ms 1
175183
```
176184

177185
GitHub Actions (`.github/workflows/tests.yml`) runs the same no-GPU path on every push.
178186

179-
Live protocol (workstation with Ollama):
187+
Live protocol (workstation with Ollama). If the server is down, `--live`
188+
prints `SKIP live` and exits 0. That skip is not a model-quality pass:
180189

181190
```bash
182191
.venv/bin/python scripts/run_eval.py --live --model fast
192+
.venv/bin/python scripts/run_eval.py --live --suite harder --model fast
193+
.venv/bin/python scripts/prove_multifile_refactor.py --live --model fast
183194
.venv/bin/python scripts/run_eval.py --live --model strong
184195
```
185196

@@ -189,7 +200,9 @@ Record live rows in `docs/phase3-log.md` with result
189200

190201
`scripts/prove_refactor_acceptance.py` remains the original single-case
191202
live script. It now uses the shared fence extractor. Prefer
192-
`scripts/run_eval.py --live --case whitespace_extract` for new runs.
203+
`scripts/run_eval.py --live --case whitespace_extract` for that seed.
204+
The harder multi-file suite is
205+
`scripts/prove_multifile_refactor.py` (fixtures by default).
193206

194207
## What this may claim later
195208

@@ -198,6 +211,9 @@ After repeated live runs, a paper may claim:
198211
- Layer-conditional rates on this corpus (format vs structure vs behavior).
199212
- That a vaguer prompt raises structure failures on the same oracle
200213
(`whitespace_extract` vs `whitespace_extract_vague`).
214+
- That leftover type/field aliases, catch-site drift, and a helper
215+
signature change that breaks a public facade are distinguishable
216+
layers on the harder multi-file suite.
201217
- That shape-only test generation overstates success relative to
202218
executed tests (`test_add_execute`; A6 now uses this checker).
203219
- That keep-vs-delegate and accept/rewrite/reject are enforceable as a

‎docs/phase3-log.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Cloud-safe stub + apply-gate run:
1616
| 2026-09-06 | local_refactor | fast | extract module-level whitespace helper | accepted | Real Ollama; generated module parsed and preserved behavior across 3 executed cases; warm run 6.7s |
1717
| 2026-09-06 | local_refactor | strong | extract module-level whitespace helper | accepted | Real Ollama; generated module parsed and preserved behavior across 3 executed cases; warm run 25.6s; model reported 46% CPU / 54% GPU at 16K context |
1818
| 2026-09-07 | all local_* eval tools | stub | 10-case corpus + apply gate | accepted / rewritten / rejected per job | Cloud Agent; no GPU; A6 now executes tests |
19+
| 2026-09-10 | local_refactor | fixtures | harder multi-file suite (3 cases) | scorer only | Offline corpus + runner; no live rate; Halo not started |
1920

2021
Columns:
2122

‎docs/roadmap.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ LAN addresses, or SKUs here.
1010
| --- | --- | --- |
1111
| 1 — inference host | Done on the workstation lab | Ollama + starter tags on a private GPU |
1212
| 2 — MCP bridge | Done | `local-coding-slm` stdio tools; Cursor / Copilot / Claude adapters |
13-
| 3 — measure | Protocol + expanded corpus + MCP apply gate + CI; live rates still informal | Layered scoring; scripted premium routing/review; no auto-classifier yet |
13+
| 3 — measure | Protocol + expanded corpus + harder multi-file suite + MCP apply gate + CI; live rates still informal | Layered scoring; scripted premium routing/review; no auto-classifier yet |
1414
| T12 Part A second NVIDIA host | Blocked on host power | WSL GPU via SSH; see [examples/downstairs-wsl-gpu.md](../examples/downstairs-wsl-gpu.md) |
1515
| T12 Part B Copilot A8 / Claude A9 | Config ready; operator clicks pending | Same-machine only; [docs/a8-a9-operator-checklist.md](a8-a9-operator-checklist.md) |
1616

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env python3
2+
"""Score the harder multi-file refactor corpus.
3+
4+
Default path is offline fixtures (CI / no GPU). ``--live`` calls real
5+
``local_refactor`` through stdio MCP and skips with exit 0 when Ollama
6+
is down. A skip is not a model-quality pass.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import argparse
12+
import subprocess
13+
import sys
14+
from pathlib import Path
15+
16+
ROOT = Path(__file__).resolve().parents[1]
17+
18+
19+
def main() -> None:
20+
parser = argparse.ArgumentParser(description=__doc__)
21+
parser.add_argument("--live", action="store_true")
22+
parser.add_argument("--model", choices=("fast", "strong"), default="fast")
23+
parser.add_argument("--case", dest="case_id", default=None)
24+
args = parser.parse_args()
25+
cmd = [
26+
sys.executable,
27+
str(ROOT / "scripts" / "run_eval.py"),
28+
"--suite",
29+
"harder",
30+
]
31+
if args.live:
32+
cmd.extend(["--live", "--model", args.model])
33+
if args.case_id:
34+
cmd.extend(["--case", args.case_id])
35+
raise SystemExit(subprocess.call(cmd, cwd=str(ROOT)))
36+
37+
38+
if __name__ == "__main__":
39+
main()

‎scripts/prove_refactor_acceptance.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#!/usr/bin/env python3
2-
"""Run a semantic local_refactor check against the configured Ollama runtime."""
2+
"""Run a semantic local_refactor check against the configured Ollama runtime.
3+
4+
Single-file whitespace extract only. For the harder multi-file corpus,
5+
use ``scripts/prove_multifile_refactor.py`` (offline fixtures by default;
6+
``--live`` skips when Ollama is down).
7+
"""
38

49
from __future__ import annotations
510

‎scripts/run_eval.py‎

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,21 @@
1414
if str(SRC) not in sys.path:
1515
sys.path.insert(0, str(SRC))
1616

17-
from local_coding_slm.eval.cases import CASES, CASES_BY_ID, FIXTURES # noqa: E402
17+
from local_coding_slm.eval.cases import ( # noqa: E402
18+
CASES,
19+
CASES_BY_ID,
20+
FIXTURES,
21+
SEED_CASE_IDS,
22+
)
23+
from local_coding_slm.eval.cases_harder import HARDER_CASE_IDS # noqa: E402
1824
from local_coding_slm.eval.score import score_candidate # noqa: E402
1925

26+
SUITE_CASE_IDS = {
27+
"all": None,
28+
"seed": set(SEED_CASE_IDS),
29+
"harder": set(HARDER_CASE_IDS),
30+
}
31+
2032

2133
def _print_result(label: str, result: object) -> None:
2234
passed = getattr(result, "passed")
@@ -30,11 +42,20 @@ def _print_result(label: str, result: object) -> None:
3042
print(f" {layer.status:4} {layer.name}: {layer.message}")
3143

3244

33-
def _run_fixtures(case_id: str | None) -> int:
45+
def _wanted(case_id: str, suite: str, only: str | None) -> bool:
46+
if only and case_id != only:
47+
return False
48+
allowed = SUITE_CASE_IDS[suite]
49+
return allowed is None or case_id in allowed
50+
51+
52+
def _run_fixtures(case_id: str | None, suite: str) -> int:
3453
failed = 0
54+
ran = 0
3555
for fixture in FIXTURES:
36-
if case_id and fixture.case_id != case_id:
56+
if not _wanted(fixture.case_id, suite, case_id):
3757
continue
58+
ran += 1
3859
case = CASES_BY_ID[fixture.case_id]
3960
result = score_candidate(fixture.text, case)
4061
ok = result.passed == fixture.expect_pass
@@ -54,13 +75,25 @@ def _run_fixtures(case_id: str | None) -> int:
5475
if not ok:
5576
print(f" expected pass={fixture.expect_pass} first={fixture.expect_first}")
5677
failed += 1
78+
if ran == 0:
79+
print("no fixtures matched")
80+
return 1
5781
return failed
5882

5983

60-
async def _run_live(case_id: str | None, model: str) -> int:
84+
async def _run_live(case_id: str | None, model: str, suite: str) -> int:
85+
from local_coding_slm.ollama_client import OllamaSettings, is_reachable
6186
from local_coding_slm.server import _load_dotenv
6287

6388
_load_dotenv()
89+
settings = OllamaSettings.from_env()
90+
if not is_reachable(settings):
91+
print(
92+
f"SKIP live: Ollama unreachable at {settings.host_label()} "
93+
"(offline fixtures still pass; this is not a model-quality fail)"
94+
)
95+
return 0
96+
6497
from mcp import ClientSession, StdioServerParameters
6598
from mcp.client.stdio import stdio_client
6699

@@ -73,7 +106,12 @@ async def _run_live(case_id: str | None, model: str) -> int:
73106
cwd=str(ROOT),
74107
)
75108
failed = 0
76-
selected = [case for case in CASES if case_id is None or case.id == case_id]
109+
selected = [
110+
case for case in CASES if _wanted(case.id, suite, case_id)
111+
]
112+
if not selected:
113+
print("no live cases matched")
114+
return 1
77115
async with stdio_client(params) as (read, write):
78116
async with ClientSession(read, write) as session:
79117
await session.initialize()
@@ -83,7 +121,7 @@ async def _run_live(case_id: str | None, model: str) -> int:
83121
"files": list(case.files),
84122
"language": case.language,
85123
"model": model,
86-
"max_tokens": 700,
124+
"max_tokens": case.max_tokens,
87125
}
88126
if case.style:
89127
payload["style"] = case.style
@@ -107,10 +145,16 @@ def main() -> None:
107145
parser.add_argument("--live", action="store_true", help="Call real Ollama via stdio MCP")
108146
parser.add_argument("--model", choices=("fast", "strong"), default="fast")
109147
parser.add_argument("--case", dest="case_id", default=None)
148+
parser.add_argument(
149+
"--suite",
150+
choices=tuple(SUITE_CASE_IDS),
151+
default="all",
152+
help="Fixture / live subset. harder = Phase 3 multi-file behavior cases.",
153+
)
110154
args = parser.parse_args()
111155
if args.live:
112-
raise SystemExit(asyncio.run(_run_live(args.case_id, args.model)))
113-
failed = _run_fixtures(args.case_id)
156+
raise SystemExit(asyncio.run(_run_live(args.case_id, args.model, args.suite)))
157+
failed = _run_fixtures(args.case_id, args.suite)
114158
raise SystemExit(1 if failed else 0)
115159

116160

‎spec.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,9 @@ OLLAMA_BASE_URL
662662
│ ├── envfile.py
663663
│ ├── prompts.py
664664
│ └── eval/ ← scorer, corpus, stub Ollama, harness
665+
│ ├── cases.py
666+
│ ├── cases_extended.py
667+
│ └── cases_harder.py ← multi-file behavior-preservation suite
665668
├── docs/
666669
│ ├── roadmap.md ← current vs future phases
667670
│ ├── c4.md ← C4 context / container / component
@@ -672,6 +675,7 @@ OLLAMA_BASE_URL
672675
│ ├── run_mcp.sh ← project MCP entry (loads .env)
673676
│ ├── prove_acceptance.py
674677
│ ├── prove_refactor_acceptance.py
678+
│ ├── prove_multifile_refactor.py ← harder multi-file suite; --live skips if down
675679
│ ├── run_eval.py ← fixture corpus; --live on the workstation
676680
│ ├── run_harness.py ← timed MCP campaign (stub or live)
677681
│ └── check_deployment_safety.py ← defensive bind / tag / git checks
@@ -681,6 +685,7 @@ OLLAMA_BASE_URL
681685
├── test_safety.py
682686
├── test_eval_extract.py
683687
├── test_eval_score.py
688+
├── test_eval_harder.py
684689
└── test_eval_harness.py
685690
```
686691

0 commit comments

Comments
 (0)