diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2ec56..7547442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,13 @@ All notable changes to vouch are documented here. Format follows - console Dashboard view: 12-month activity calendar, last-30-days bars, hour-of-week heatmap, top actors and event mix, driven by `kb.activity`. +### Fixed +- the JSONL / HTTP `/rpc` envelope misreported lifecycle guard failures + (e.g. `kb.supersede` of a claim onto itself) as `internal_error` and + logged a full server-side traceback for what is a bad request — + `LifecycleError` now maps to `invalid_request` like `ProposalError`, + matching how the CLI already surfaces it. + ## [1.2.2] — 2026-07-07 ### Packaging diff --git a/src/vouch/jsonl_server.py b/src/vouch/jsonl_server.py index 43a1472..633e250 100644 --- a/src/vouch/jsonl_server.py +++ b/src/vouch/jsonl_server.py @@ -39,6 +39,7 @@ from . import verify as verify_mod from .capabilities import capabilities as build_caps from .context import build_context_pack +from .lifecycle import LifecycleError from .logging_config import configure_logging from .models import ProposalStatus from .page_filters import filter_pages @@ -891,7 +892,12 @@ def handle_request(envelope: dict) -> dict: "id": req_id, "ok": False, "error": {"code": "missing_param", "message": str(e)}, } - except (ValueError, ProposalError, ArtifactNotFoundError) as e: + except (ValueError, ProposalError, LifecycleError, ArtifactNotFoundError) as e: + # Caller-visible domain errors. LifecycleError belongs here like + # ProposalError does (both are RuntimeError subclasses the generic + # branch would otherwise misreport as internal_error): a lifecycle + # guard such as "a claim cannot supersede itself" is a bad request, + # not a server fault. The CLI already maps it via _cli_errors. return { "id": req_id, "ok": False, "error": {"code": "invalid_request", "message": str(e)}, diff --git a/tests/test_jsonl_server.py b/tests/test_jsonl_server.py index dc1aa0a..a8e0652 100644 --- a/tests/test_jsonl_server.py +++ b/tests/test_jsonl_server.py @@ -248,3 +248,27 @@ def _boom(_params: dict) -> dict: assert resp["ok"] is False assert resp["error"]["code"] == "internal_error" assert "traceback" not in resp["error"] + + +def test_jsonl_lifecycle_guard_maps_to_invalid_request( + store: KBStore, monkeypatch, +) -> None: + """Lifecycle guards are caller conditions, not server faults. + + `kb.supersede` of a claim onto itself raises LifecycleError, which the + envelope used to misreport as internal_error (and log a full server-side + traceback for what is a bad request). It must map to invalid_request, + exactly as the CLI already surfaces it via _cli_errors.""" + monkeypatch.chdir(store.root) + src = store.put_source(b"e") + store.put_claim(Claim(id="c1", text="a durable claim", evidence=[src.id])) + resp = handle_request({ + "id": "r1", "method": "kb.supersede", + "params": {"old_claim_id": "c1", "new_claim_id": "c1"}, + }) + assert resp["ok"] is False + assert resp["error"]["code"] == "invalid_request" + assert "cannot supersede itself" in resp["error"]["message"] + # the guard fired before any write — the claim is untouched + assert store.get_claim("c1").status.value == "working" + assert store.get_claim("c1").superseded_by is None