From 56c8e10bc52f9c4d1c76b2f559a481eea6bcce54 Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Fri, 3 Jul 2026 16:54:18 -0400 Subject: [PATCH] fix(mcp): report dirty freshness for architecture Add canonical-only dirty freshness metadata to get_architecture responses so architecture summaries stay visible while pending dirty files are disclosed. Cover the behavior with an MCP regression test and validate with focused architecture canary, full MCP suite, source-safety, diff hygiene, and product build logs under /private/tmp. Signed-off-by: Andrew Hundt --- src/mcp/mcp.c | 8 ++++++++ tests/test_mcp.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 65dc262a4..fba98f68a 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -4278,6 +4278,14 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) { add_stale_derived_view_warning(doc, root, CBM_STORE_DERIVED_VIEW_ROUTES, "routes derived view is stale; route results may be stale."); } + int dirty_pending = 0; + int dirty_overlay_ready = 0; + if (get_dirty_file_counts(store, project, &dirty_pending, &dirty_overlay_ready)) { + add_dirty_file_freshness_counts(doc, root, dirty_pending, dirty_overlay_ready); + add_response_warning(doc, root, + "get_architecture reads canonical graph summaries; dirty file " + "changes may be absent until overlay or reindex completes."); + } /* Node label summary */ if (aspect_wanted(aspects_doc, aspects_arr, "structure")) { diff --git a/tests/test_mcp.c b/tests/test_mcp.c index f2c578916..a4f4956e3 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -1767,6 +1767,51 @@ TEST(tool_get_architecture_warns_on_stale_derived_views) { PASS(); } +TEST(tool_get_architecture_reports_dirty_metadata_as_canonical_only) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_store_t *st = cbm_mcp_server_store(srv); + ASSERT_NOT_NULL(st); + + const char *proj = "arch-dirty"; + ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/arch-dirty"), CBM_STORE_OK); + cbm_mcp_server_set_project(srv, proj); + + cbm_node_t fn = {.project = proj, + .label = "Function", + .name = "Run", + .qualified_name = "arch-dirty.Run", + .file_path = "run.c", + .properties_json = "{\"is_entry_point\":true}"}; + ASSERT_GT(cbm_store_upsert_node(st, &fn), 0); + + cbm_dirty_file_state_t dirty = {.project = proj, + .rel_path = "run.c", + .observed_hash = "arch-dirty-hash", + .observed_generation = 13, + .source = CBM_STORE_DIRTY_SOURCE_EXPLICIT_REINDEX, + .status = CBM_STORE_DIRTY_STATUS_PENDING}; + ASSERT_EQ(cbm_store_upsert_dirty_file(st, &dirty), CBM_STORE_OK); + + char *resp = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":95,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"get_architecture\"," + "\"arguments\":{\"project\":\"arch-dirty\",\"aspects\":[\"all\"]}}}"); + ASSERT_NOT_NULL(resp); + char *inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_NOT_NULL(strstr(inner, "\"entry_points\"")); + ASSERT_NOT_NULL(strstr(inner, "Run")); + ASSERT_NOT_NULL(strstr(inner, "\"warnings\"")); + ASSERT_NOT_NULL(strstr(inner, "get_architecture reads canonical graph summaries")); + ASSERT_TRUE(has_dirty_freshness_counts(inner, 1, 0)); + + free(inner); + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + TEST(tool_get_architecture_path_scoping) { cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); ASSERT_NOT_NULL(srv); @@ -3995,6 +4040,7 @@ SUITE(mcp) { RUN_TEST(tool_get_architecture_empty); RUN_TEST(tool_get_architecture_emits_populated_sections); RUN_TEST(tool_get_architecture_warns_on_stale_derived_views); + RUN_TEST(tool_get_architecture_reports_dirty_metadata_as_canonical_only); RUN_TEST(tool_get_architecture_path_scoping); RUN_TEST(tool_query_graph_missing_query);