From 92c65621628a5464ad59ba7c22beed188ff57156 Mon Sep 17 00:00:00 2001 From: eldad-caura Date: Sun, 4 Oct 2026 08:25:24 +0300 Subject: [PATCH] docs(api): say superseded_by is the older memory this one replaced GET /memories/{id}/contradictions fills superseded_by from the memory's supersedes_id, which is the older row this memory replaced. The route's own comment says so, and MCP caura_manage op=lineage returns the key the same way. The OpenAPI description said the opposite, "the newer memory that superseded this one" (M-101). public-api-stability.md makes response shapes part of the contract, so a client built from the published schema walked the chain backwards: it read the live winner as stale and the retired row as its correction. The description now matches what the route returns, and points to superseded_memories for the newer rows, which now carries a description of its own. Co-Authored-By: Claude Opus 5.5 Signed-off-by: eldad-caura --- core-api/src/core_api/openapi_responses.py | 10 +++++-- ...t_contradictions_superseded_by_contract.py | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/test_contradictions_superseded_by_contract.py diff --git a/core-api/src/core_api/openapi_responses.py b/core-api/src/core_api/openapi_responses.py index 747bf9450..a054ddec5 100644 --- a/core-api/src/core_api/openapi_responses.py +++ b/core-api/src/core_api/openapi_responses.py @@ -94,9 +94,15 @@ class MemoryContradictionsResponse(BaseModel): memory_id: str status: str | None superseded_by: SupersessionPeer | None = Field( - description="The newer memory that superseded this one; null when none is live." + description=( + "The older memory this one superseded (via supersedes_id); null when there is none or " + "it was deleted. The field name is kept for back-compat; newer memories that superseded " + "this one are in superseded_memories and in contradictions with direction superseded_by." + ) + ) + superseded_memories: list[SupersessionPeer] = Field( + description="Newer memories that superseded this one, each with supersedes_id pointing here." ) - superseded_memories: list[SupersessionPeer] detection_status: str = Field(description="completed or pending.") contradictions: list[ContradictionEntry] diff --git a/tests/test_contradictions_superseded_by_contract.py b/tests/test_contradictions_superseded_by_contract.py new file mode 100644 index 000000000..aa3869ba1 --- /dev/null +++ b/tests/test_contradictions_superseded_by_contract.py @@ -0,0 +1,28 @@ +"""The published schema says which way ``superseded_by`` points (M-101). + +``GET /memories/{id}/contradictions`` fills ``superseded_by`` from this memory's +``supersedes_id``: the OLDER row this memory replaced. The route's own comment +says so, and MCP ``caura_manage op=lineage`` returns the same key the same way. +The OpenAPI description said the opposite ("the newer memory that superseded +this one"), so a client built from the published schema walked the chain +backwards: it read the live winner as stale and the retired row as its +correction. ``tests/test_api_contradictions.py`` pins the behaviour; this pins +the contract text to it. +""" + +from core_api.app import app + + +def _properties() -> dict: + schemas = app.openapi()["components"]["schemas"] + return schemas["MemoryContradictionsResponse"]["properties"] + + +def test_superseded_by_is_documented_as_the_older_memory(): + description = _properties()["superseded_by"].get("description", "") + assert description.startswith("The older memory this one superseded"), description + + +def test_superseded_memories_is_documented_as_the_newer_ones(): + description = _properties()["superseded_memories"].get("description", "") + assert description.startswith("Newer memories that superseded this"), description