diff --git a/src/tracer/api.py b/src/tracer/api.py index af0bba9..e04ad2c 100644 --- a/src/tracer/api.py +++ b/src/tracer/api.py @@ -232,17 +232,23 @@ def update( existing_index = EmbeddingIndex.load(artifact_dir / "index") X_combined = np.vstack([existing_index.embeddings, new_embeddings.astype(np.float32)]) - # Load existing traces (we need to re-save combined) + # Load existing traces so we can re-save the combined set. fit() always + # writes all_traces.jsonl, so it should be present. If it is missing (e.g. + # it was deleted), the original records cannot be recovered from the index + # embeddings alone, and appending only the new records would leave the trace + # set out of sync with X_combined. Fail fast with an actionable message + # instead of surfacing a cryptic trace/embedding mismatch from fit(). existing_traces_path = artifact_dir / "all_traces.jsonl" - if existing_traces_path.exists(): - from tracer.traces.loader import load_traces as _lt - existing_ds = _lt(existing_traces_path) - combined_records = existing_ds.records + new_ds.records - else: - # First update: reconstruct original traces from the initial fit - # by loading them from the original trace source if we can find it, - # or infer count from existing embeddings - combined_records = new_ds.records + if not existing_traces_path.exists(): + raise FileNotFoundError( + f"{existing_traces_path} not found — cannot continue continual " + "learning. fit() writes this file; if it was removed, re-run " + "tracer.fit() on your full trace set to rebuild it before calling " + "update().") + + from tracer.traces.loader import load_traces as _lt + existing_ds = _lt(existing_traces_path) + combined_records = existing_ds.records + new_ds.records from tracer.types import TraceDataset from tracer.traces.loader import save_traces diff --git a/tests/test_fit.py b/tests/test_fit.py index bc55279..2f8231a 100644 --- a/tests/test_fit.py +++ b/tests/test_fit.py @@ -194,6 +194,30 @@ def test_update(): assert updated.manifest.n_traces == 300 # 200 + 100 +def test_update_missing_all_traces_raises_clear_error(): + """If all_traces.jsonl is gone, update() should fail fast with a clear + message rather than a cryptic trace/embedding mismatch.""" + from tracer.api import fit, update + with tempfile.TemporaryDirectory() as tmp: + path, X, _ = _make_traces(tmp, n=200) + artifact_dir = Path(tmp) / ".tracer" + fit(path, artifact_dir=artifact_dir) + + # Simulate a user deleting the file AGENTS.md warns against removing. + (artifact_dir / "all_traces.jsonl").unlink() + + new_path = Path(tmp) / "new_traces.jsonl" + rng = np.random.RandomState(99) + X_new = rng.randn(50, 32).astype(np.float32) + with new_path.open("w") as f: + for i in range(50): + f.write(json.dumps({"input": f"new {i}", "teacher": f"cls_{i % 4}"}) + "\n") + np.save(new_path.with_suffix(".npy"), X_new) + + with pytest.raises(FileNotFoundError, match="all_traces.jsonl"): + update(new_path, artifact_dir=artifact_dir) + + # ── Qualitative report ──────────────────────────────────────────────────────── def test_qualitative_report(): @@ -376,6 +400,7 @@ def test_public_exports(): test_fit_missing_embeddings_raises, test_router_wrong_dim_raises, test_update, + test_update_missing_all_traces_raises_clear_error, test_qualitative_report, test_qualitative_report_boundary_pairs, test_report,