diff --git a/docs/user-guide.md b/docs/user-guide.md index f5cd100..aa8b7d4 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -207,6 +207,12 @@ Run `get_corpus_readiness` to distinguish a service problem from a valid empty result. Connection errors identify the configured API origin without including access tokens. +Readiness compares the code index with the API's code-search catalog checkpoint. +The broader corpus checkpoint can advance after enrichment without changing code +content. A difference from that broader checkpoint does not establish an index +problem. If the catalog checkpoint is absent, the brief makes no checkpoint +comparison and preserves the API's reported subsystem status. + ### A page cursor is rejected Repeat the same tool with the same query, filters, sort, and limit. Copy diff --git a/pyproject.toml b/pyproject.toml index c8bdc6c..fe82c13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "eip-mcp" -version = "3.0.1" +version = "3.0.2" description = "MCP server for the Exploit Intelligence Platform v3 research API" readme = "README.md" requires-python = ">=3.12" diff --git a/src/eip_mcp_v3/format_common.py b/src/eip_mcp_v3/format_common.py index fcf997c..09b6317 100644 --- a/src/eip_mcp_v3/format_common.py +++ b/src/eip_mcp_v3/format_common.py @@ -21,11 +21,9 @@ "alone." ) -INDEX_BEHIND_CORPUS = ( - "This checkpoint differs from the corpus source checkpoint above: the index " - "was built from a different snapshot, so a code-search result may omit " - "artifacts the rest of this server can see. EIP states the difference and " - "makes no claim about which is newer." +INDEX_CATALOG_MISMATCH = ( + "The index checkpoint differs from the API's code-search catalog checkpoint. " + "Code search is not aligned with the current catalog." ) UNDATED_TOTALS = ( diff --git a/src/eip_mcp_v3/format_system.py b/src/eip_mcp_v3/format_system.py index f93c37c..dc6d393 100644 --- a/src/eip_mcp_v3/format_system.py +++ b/src/eip_mcp_v3/format_system.py @@ -9,7 +9,7 @@ _FILE_LIMIT, _POINT_LIMIT, _SERIES_LIMIT, - INDEX_BEHIND_CORPUS, + INDEX_CATALOG_MISMATCH, MANIFEST_UNREACHABLE, UNDATED_TOTALS, VIEWABILITY_POLICY, @@ -195,18 +195,18 @@ def format_readiness(data: dict[str, Any]) -> str: built_at = inline(data.get("code_search_built_at"), max_len=40) if built_at: lines.append(f"- Index built at: {built_at}") - # The one field that says *what* the index was built from. `code_search_status: - # ready` is subsystem health - an index built from a stale checkpoint reports - # `ready` exactly like a current one, and this page's own tool description - # promises it distinguishes an empty result from a degraded index. Without the - # checkpoint the two build times sit side by side with nothing reconciling them, - # and an empty `search_exploit_code` result reads as corpus absence. + # Optional enrichment can advance the corpus checkpoint without changing the + # code catalog. Only the catalog checkpoint is comparable to the index. index_checkpoint = inline(data.get("code_search_checkpoint_sha256"), max_len=88) if index_checkpoint: lines.append(f"- Index checkpoint: {index_checkpoint}") - source_checkpoint = inline(data.get("source_checkpoint_sha256"), max_len=88) - if source_checkpoint and source_checkpoint != index_checkpoint: - lines.append(f"- {INDEX_BEHIND_CORPUS}") + catalog_checkpoint = inline(data.get("code_search_catalog_checkpoint_sha256"), max_len=88) + if catalog_checkpoint: + lines.append(f"- Catalog checkpoint: {catalog_checkpoint}") + if index_checkpoint and data["code_search_catalog_checkpoint_sha256"] != data.get( + "code_search_checkpoint_sha256" + ): + lines.append(f"- {INDEX_CATALOG_MISMATCH}") for label, key in ( ("Artifacts indexed", "code_search_artifact_count"), ("Files indexed", "code_search_file_count"), diff --git a/tests/test_format_vulnerability.py b/tests/test_format_vulnerability.py index 16c9609..bc72552 100644 --- a/tests/test_format_vulnerability.py +++ b/tests/test_format_vulnerability.py @@ -6,7 +6,7 @@ import eip_mcp_v3.format as fmt from eip_mcp_v3.format import SOURCE_DATE_RULE, VULN_SECTIONS, format_vulnerability -from eip_mcp_v3.format_common import INDEX_BEHIND_CORPUS +from eip_mcp_v3.format_common import INDEX_CATALOG_MISMATCH from eip_mcp_v3.format_system import format_readiness, format_statistics from eip_mcp_v3.text import UNTRUSTED_NOTE @@ -2048,34 +2048,57 @@ def test_a_dated_trend_page_does_not_carry_the_undated_notice(): assert "2026-08-04" in page -# `code_search_status: ready` is subsystem health, not freshness. An index built -# from an older checkpoint reports ready exactly like a current one, and an empty -# code-search result then reads as corpus absence. -def test_readiness_states_the_checkpoint_the_index_was_built_from(): +def test_readiness_compares_catalog_even_when_enrichment_changes_corpus(): page = format_readiness( { "status": "ready", "source_checkpoint_sha256": "sha256:aaa", "code_search_status": "ready", - "code_search_checkpoint_sha256": "sha256:aaa", + "code_search_checkpoint_sha256": "sha256:bbb", + "code_search_catalog_checkpoint_sha256": "sha256:bbb", } ) assert "Index checkpoint" in page - assert INDEX_BEHIND_CORPUS not in page + assert "Catalog checkpoint" in page + assert INDEX_CATALOG_MISMATCH not in page -def test_readiness_flags_an_index_built_from_a_different_snapshot(): +def test_readiness_flags_a_catalog_mismatch_even_when_corpus_matches_index(): page = format_readiness( { "status": "ready", "source_checkpoint_sha256": "sha256:aaa", "code_search_status": "ready", - "code_search_checkpoint_sha256": "sha256:bbb", + "code_search_checkpoint_sha256": "sha256:aaa", + "code_search_catalog_checkpoint_sha256": "sha256:bbb", } ) - assert INDEX_BEHIND_CORPUS in page - # EIP must not say which snapshot is newer; it has no basis for that. - assert "newer" in INDEX_BEHIND_CORPUS and "makes no claim" in INDEX_BEHIND_CORPUS + assert INDEX_CATALOG_MISMATCH in page + + +@pytest.mark.parametrize( + "missing", ["code_search_catalog_checkpoint_sha256", "code_search_checkpoint_sha256"] +) +def test_readiness_does_not_infer_alignment_from_missing_checkpoints(missing): + data = { + "source_checkpoint_sha256": "sha256:aaa", + "code_search_status": "unavailable", + "code_search_catalog_checkpoint_sha256": "sha256:bbb", + "code_search_checkpoint_sha256": "sha256:ccc", + } + del data[missing] + page = format_readiness(data) + assert INDEX_CATALOG_MISMATCH not in page + assert "fail closed" in page + + +def test_readiness_compares_full_checkpoint_values_before_display_truncation(): + data = { + "code_search_status": "ready", + "code_search_catalog_checkpoint_sha256": "x" * 100 + "a", + "code_search_checkpoint_sha256": "x" * 100 + "b", + } + assert INDEX_CATALOG_MISMATCH in format_readiness(data) # SSVC was the only line under `## Exploitation context` rendered with no source