Skip to content

Commit 18ad729

Browse files
committed
fix(fit,runtime): guard against all-candidate failures and stale pipeline (0.3.3)
When all surrogate candidates fail to train, search_best_surrogate() returns (None, None, None). build_global() and _build_accepting_stage() now check for a None clf before proceeding and return a structured failure state instead of crashing with a TypeError. Router.load() would silently load a stale pipeline.joblib left on disk from a previous fit, even when manifest.json reports selected_method: null. It now raises a clear ValueError explaining the state so the caller is never silently served a pipeline the parity gate already rejected.
1 parent bc483c7 commit 18ad729

5 files changed

Lines changed: 32 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
All notable changes to TRACER are recorded here. This project follows semantic
44
versioning.
55

6+
## 0.3.3 (2026-06)
7+
8+
### Fixed
9+
- `build_global()` and `_build_accepting_stage()` no longer crash with a
10+
`TypeError` when all surrogate candidates fail to train. They now return a
11+
structured failure state, consistent with every other non-deployable path.
12+
- `Router.load()` now raises a clear `ValueError` when `manifest.json`
13+
reports `selected_method: null`, instead of silently loading a stale
14+
`pipeline.joblib` left on disk from a previous fit.
15+
16+
## 0.3.2 (2026-06)
17+
18+
### Fixed
19+
- `load_traces()` now reports the correct 1-indexed file line number in
20+
`ValueError` messages. Previously the count excluded blank lines, so
21+
the reported line was lower than the actual position of the bad record.
22+
623
## 0.3.1 (2026-06)
724

825
### Changed

‎src/tracer/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def __getattr__(name: str):
7878
return obj
7979

8080

81-
__version__ = "0.3.2"
81+
__version__ = "0.3.3"
8282

8383
__all__ = [
8484
# Core API

‎src/tracer/cloud/client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class CloudError(RuntimeError):
3939

4040
# A real User-Agent: the default "Python-urllib/x" trips Cloudflare's bot
4141
# protection on app.tracerml.ai (HTTP 403, CF error 1010).
42-
USER_AGENT = "tracer-cli/0.3.2 (+https://tracerml.ai)"
42+
USER_AGENT = "tracer-cli/0.3.3 (+https://tracerml.ai)"
4343

4444

4545
def _http(

‎src/tracer/fit/pipeline.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ def build_global(split, target_ta, alpha: float = 0.1, log: Optional[LogFn] = No
252252
clf, name, val_m = search_best_surrogate(
253253
split["X_train"], split["y_train"], split["X_val"], split["y_val"],
254254
on_candidate=_candidate_log(log), skip=skip)
255+
if clf is None:
256+
log(" build_global: all surrogate candidates failed (no model trained)")
257+
return {"method": "global", "stages": [], "summary": {
258+
"status": "no_stage", "coverage_cal_total": 0.0}}
255259
log(f" build_global: surrogate done in {time.perf_counter()-t0:.1f}s best={name} f1={val_m['teacher_f1']:.3f}")
256260
preds_cal = clf.predict(split["X_cal"])
257261
n_cal = len(preds_cal)
@@ -297,6 +301,9 @@ def _build_accepting_stage(X_tr, y_tr, X_val, y_val, X_cal, y_cal, target_ta, st
297301
log(f" {stage_name}: surrogate sweep on {len(X_tr)} train / {len(X_val)} val")
298302
clf, name, val_m = search_best_surrogate(
299303
X_tr, y_tr, X_val, y_val, on_candidate=_candidate_log(log), skip=skip)
304+
if clf is None:
305+
log(f" {stage_name}: all surrogate candidates failed (no model trained)")
306+
return None
300307
log(f" {stage_name}: surrogate done in {time.perf_counter()-t0:.1f}s best={name} f1={val_m['teacher_f1']:.3f}")
301308
preds_val, probs_val = _predict(clf, X_val)
302309
preds_cal, probs_cal = _predict(clf, X_cal)

‎src/tracer/runtime/router.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ def load(cls, artifact_dir: Union[str, Path], embedder=None) -> "Router":
5656
import json as _json
5757
artifact_dir = Path(artifact_dir)
5858
manifest = load_manifest(artifact_dir / "manifest.json")
59+
if manifest.selected_method is None:
60+
raise ValueError(
61+
f"No router is deployed in {artifact_dir}: the parity gate did not "
62+
"certify any method (selected_method is null in manifest.json). "
63+
"Run tracer.fit() again with more data or a lower target_teacher_agreement."
64+
)
5965
bundle = load_pipeline(artifact_dir)
6066
stages = bundle["pipeline"]["stages"]
6167
label_space = bundle["label_space"]

0 commit comments

Comments
 (0)