From d5e0fe73d9c9740b0c26b3a5418ccb528af137ca Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 9 Sep 2026 01:40:44 +0000 Subject: [PATCH] refactor(dashboard): wire PR autotrack through reader callback Delete the Dashboard/Daemon PR-autotrack read port pair and project managed_summary through a root-addressed callback on dashboard state. --- .../src/events_api.rs | 1 + crates/tracedecay-dashboard-api/src/lib.rs | 16 +++- .../src/settings_api.rs | 85 +++++++++++++------ crates/tracedecay/src/config.rs | 31 +------ .../src/daemon/pr_autotrack/tests.rs | 84 ++++++++++++++++++ .../src/daemon/project_composition.rs | 19 +++++ crates/tracedecay/src/mcp/server.rs | 4 + .../tracedecay/src/mcp/server/construction.rs | 13 +++ .../src/mcp/server/requests/tool_dispatch.rs | 1 + .../src/mcp/tools/handlers/dashboard.rs | 2 + .../src/mcp/tools/handlers/dispatch_groups.rs | 1 + .../tracedecay/src/mcp/tools/handlers/mod.rs | 3 + 12 files changed, 201 insertions(+), 59 deletions(-) diff --git a/crates/tracedecay-dashboard-api/src/events_api.rs b/crates/tracedecay-dashboard-api/src/events_api.rs index cb699e17b4..42d4ee492d 100644 --- a/crates/tracedecay-dashboard-api/src/events_api.rs +++ b/crates/tracedecay-dashboard-api/src/events_api.rs @@ -976,6 +976,7 @@ pub(crate) async fn dashboard_state_fixture( code_index_freshness_reader: None, explorer_semantic_reader: None, feedback_status_reader: None, + pr_autotrack_reader: None, storage_mode: "profile_sharded".to_owned(), store_root, config_path: project.path().join("config.json"), diff --git a/crates/tracedecay-dashboard-api/src/lib.rs b/crates/tracedecay-dashboard-api/src/lib.rs index ec82521e8e..5b9eb21ad9 100644 --- a/crates/tracedecay-dashboard-api/src/lib.rs +++ b/crates/tracedecay-dashboard-api/src/lib.rs @@ -177,9 +177,8 @@ mod settings_api; pub use settings_api::{ DashboardCodeIndexWorkerConfigurationV1, DashboardCodeIndexWorkerSettingsCommitFuture, DashboardCodeIndexWorkerSettingsCommitV1, DashboardCodeIndexWorkerSettingsErrorV1, - DashboardCodeIndexWorkerSettingsFuture, DashboardPrAutoTrackEntryV1, - DashboardPrAutoTrackReadPort, DashboardProfileCodeIndexWorkerSettingsPort, - install_dashboard_pr_autotrack_read_port, + DashboardCodeIndexWorkerSettingsFuture, DashboardProfileCodeIndexWorkerSettingsPort, + PrAutoTrackManagedSummaryEntryV1, PrAutoTrackManagedSummaryReader, }; mod storage_findings_api; mod storage_telemetry_api; @@ -351,6 +350,10 @@ pub struct DashboardStateCompositionV1 { /// it absent and the source reports typed `unsupported`. pub explorer_semantic_reader: Option, pub feedback_status_reader: Option, + /// Root-addressed read over the daemon-owned PR-autotrack state sidecar. + /// Selected projects reuse the resolver but resolve their own exact store + /// root on every call. + pub pr_autotrack_reader: Option, pub code_diagnostics_broker: Option>>, pub application_invocation_executor: Option>, @@ -463,6 +466,8 @@ pub struct DashboardState { /// observation owner. Selected projects reuse the resolver but resolve /// their own exact project root on every call. pub feedback_status_reader: Option, + /// Daemon-owned read over PR-autotrack managed branches for settings. + pub pr_autotrack_reader: Option, /// Storage mode resolved for the active project store. pub storage_mode: String, /// Resolved active project store root. @@ -776,6 +781,7 @@ async fn build_state_inner( code_index_freshness_reader, explorer_semantic_reader, feedback_status_reader, + pr_autotrack_reader, code_diagnostics_broker, application_invocation_executor, delivery_settlement_authority, @@ -848,6 +854,7 @@ async fn build_state_inner( code_index_freshness_reader, explorer_semantic_reader, feedback_status_reader, + pr_autotrack_reader, storage_mode, store_root, config_path, @@ -933,6 +940,7 @@ pub async fn build_selected_project_state( // resolves the selected state's exact root on every call. explorer_semantic_reader: active.explorer_semantic_reader.clone(), feedback_status_reader: active.feedback_status_reader.clone(), + pr_autotrack_reader: active.pr_autotrack_reader.clone(), code_diagnostics_broker: None, // Rebinding an application transport is required only for the // selected project's application routes. Ordinary read routes @@ -1077,6 +1085,7 @@ where code_index_freshness_reader: None, explorer_semantic_reader: None, feedback_status_reader: None, + pr_autotrack_reader: None, code_diagnostics_broker: Some(code_diagnostics_broker), application_invocation_executor: test_authority .and_then(|authority| authority.application_invocation_executor.clone()), @@ -2391,6 +2400,7 @@ mod authority_tests { code_index_freshness_reader: None, explorer_semantic_reader: None, feedback_status_reader: None, + pr_autotrack_reader: None, storage_mode: storage_mode_label(&layout.storage_mode).to_owned(), store_root: layout.data_root.clone(), config_path: layout.config_path.clone(), diff --git a/crates/tracedecay-dashboard-api/src/settings_api.rs b/crates/tracedecay-dashboard-api/src/settings_api.rs index 64827af460..80dff7714b 100644 --- a/crates/tracedecay-dashboard-api/src/settings_api.rs +++ b/crates/tracedecay-dashboard-api/src/settings_api.rs @@ -1,9 +1,9 @@ //! Dashboard endpoints for project and user settings. use std::future::Future; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::pin::Pin; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; use axum::Json; use axum::extract::State; @@ -296,31 +296,24 @@ struct PrAutoTrackPayloadV1 { tracked: Vec, } -#[derive(Clone, Debug, JsonSchema, Serialize)] +#[derive(Clone, Debug, PartialEq, JsonSchema, Serialize)] struct PrAutoTrackEntryV1 { branch: String, pr: u64, head_branch: String, } -#[derive(Clone, Debug)] -pub struct DashboardPrAutoTrackEntryV1 { +/// One managed PR branch projected for dashboard settings payloads. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PrAutoTrackManagedSummaryEntryV1 { pub branch: String, pub pr: u64, pub head_branch: String, } -pub trait DashboardPrAutoTrackReadPort: Send + Sync { - fn managed_summary(&self, store_root: &Path) -> Vec; -} - -static PR_AUTOTRACK_READ_PORT: OnceLock> = OnceLock::new(); - -pub fn install_dashboard_pr_autotrack_read_port( - port: Arc, -) -> Result<(), Arc> { - PR_AUTOTRACK_READ_PORT.set(port) -} +/// Root-addressed read over the daemon-owned PR-autotrack state sidecar. +pub type PrAutoTrackManagedSummaryReader = + Arc Vec + Send + Sync + 'static>; #[hotpath::measure(label = "dashboard_api.settings.get", future = true)] pub async fn get_settings(State(state): State) -> ApiResult { @@ -732,19 +725,24 @@ fn automation_settings_payload( /// Lists the PR branches the daemon currently auto-tracks for this project, read /// from the store's PR-autotrack state sidecar. Empty on non-unix or when the /// feature has tracked nothing yet. -fn pr_autotrack_payload(state: &DashboardState) -> PrAutoTrackPayloadV1 { - let tracked = PR_AUTOTRACK_READ_PORT - .get() - .map(|port| { - port.managed_summary(&state.store_root) - .into_iter() - .map(|entry| PrAutoTrackEntryV1 { - branch: entry.branch, - pr: entry.pr, - head_branch: entry.head_branch, - }) - .collect() +fn map_managed_pr_autotrack_entries( + entries: Vec, +) -> Vec { + entries + .into_iter() + .map(|entry| PrAutoTrackEntryV1 { + branch: entry.branch, + pr: entry.pr, + head_branch: entry.head_branch, }) + .collect() +} + +fn pr_autotrack_payload(state: &DashboardState) -> PrAutoTrackPayloadV1 { + let tracked = state + .pr_autotrack_reader + .as_ref() + .map(|reader| map_managed_pr_autotrack_entries(reader(state.store_root.clone()))) .unwrap_or_default(); PrAutoTrackPayloadV1 { tracked } } @@ -914,6 +912,37 @@ mod tests { ); } + #[test] + fn managed_pr_autotrack_projection_preserves_payload_fields() { + let mapped = map_managed_pr_autotrack_entries(vec![ + PrAutoTrackManagedSummaryEntryV1 { + branch: "tracedecay/autotrack/pr/1".into(), + pr: 1, + head_branch: "alpha".into(), + }, + PrAutoTrackManagedSummaryEntryV1 { + branch: "tracedecay/autotrack/pr/3".into(), + pr: 3, + head_branch: "beta".into(), + }, + ]); + assert_eq!( + mapped, + vec![ + PrAutoTrackEntryV1 { + branch: "tracedecay/autotrack/pr/1".into(), + pr: 1, + head_branch: "alpha".into(), + }, + PrAutoTrackEntryV1 { + branch: "tracedecay/autotrack/pr/3".into(), + pr: 3, + head_branch: "beta".into(), + }, + ] + ); + } + #[test] fn exact_workers_above_current_memory_safe_limit_are_a_typed_refusal() { let status = CodeIndexWorkerStatusV1 { diff --git a/crates/tracedecay/src/config.rs b/crates/tracedecay/src/config.rs index cd1939cffa..0b9a73cc83 100644 --- a/crates/tracedecay/src/config.rs +++ b/crates/tracedecay/src/config.rs @@ -928,41 +928,16 @@ impl tracedecay_configuration::config::PinnedRuntimeConfigurationCachePort } } -/// Dashboard read port over the daemon's PR-autotrack state sidecar. -struct DaemonPrAutoTrackReadPort; - -impl tracedecay_dashboard_api::DashboardPrAutoTrackReadPort for DaemonPrAutoTrackReadPort { - fn managed_summary( - &self, - store_root: &Path, - ) -> Vec { - crate::daemon::pr_autotrack::managed_summary(store_root) - .into_iter() - .map( - |entry| tracedecay_dashboard_api::DashboardPrAutoTrackEntryV1 { - branch: entry.branch, - pr: entry.pr, - head_branch: entry.head_branch, - }, - ) - .collect() - } -} - /// Installs the root-owned configuration read ports the lower crates reach -/// through their process-global slots: the pin cache, the dashboard -/// configuration reader, and the PR-autotrack reader. Idempotent. +/// through their process-global slots: the pin cache and the dashboard +/// configuration reader. Idempotent. pub(crate) fn install_usecase_runtime_configuration_authority() -> Result<()> { static INSTALLATION: LazyLock> = LazyLock::new(|| { tracedecay_configuration::config::install_pinned_runtime_configuration_cache(Arc::new( RootPinnedRuntimeConfigurationCache, )) .map_err(|error| error.to_string())?; - install_dashboard_configuration_read_port().map_err(|error| error.to_string())?; - tracedecay_dashboard_api::install_dashboard_pr_autotrack_read_port(Arc::new( - DaemonPrAutoTrackReadPort, - )) - .map_err(|_| "dashboard PR autotrack read port was already installed".to_string()) + install_dashboard_configuration_read_port().map_err(|error| error.to_string()) }); INSTALLATION .as_ref() diff --git a/crates/tracedecay/src/daemon/pr_autotrack/tests.rs b/crates/tracedecay/src/daemon/pr_autotrack/tests.rs index eea22e6906..49c2f1cd58 100644 --- a/crates/tracedecay/src/daemon/pr_autotrack/tests.rs +++ b/crates/tracedecay/src/daemon/pr_autotrack/tests.rs @@ -1349,3 +1349,87 @@ async fn cancelled_activation_keeps_its_lifecycle_owner_bounded_during_stalled_e drop(lifecycle); schedulers.shutdown().await; } + +#[test] +fn dashboard_managed_summary_reader_matches_canonical_state() { + use std::collections::BTreeMap; + use std::path::PathBuf; + use std::sync::Arc; + + let data_root = tempfile::tempdir().expect("temp data root"); + let state = PrAutotrackState { + managed: BTreeMap::from([ + ( + pr_label(3), + ManagedPr { + pr: 3, + head_branch: "feature-three".into(), + head_sha: String::new(), + worktree: PathBuf::from("/tmp/pr-3"), + tracking_ref: pr_tracking_ref(3), + }, + ), + ( + pr_label(1), + ManagedPr { + pr: 1, + head_branch: "feature-one".into(), + head_sha: String::new(), + worktree: PathBuf::from("/tmp/pr-1"), + tracking_ref: pr_tracking_ref(1), + }, + ), + ]), + }; + std::fs::write( + state_path(data_root.path()), + serde_json::to_string_pretty(&state).expect("serialize pr-autotrack state"), + ) + .expect("write pr-autotrack state"); + + let canonical = managed_summary(data_root.path()); + let reader: tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader = + Arc::new(|store_root| { + managed_summary(&store_root) + .into_iter() + .map( + |entry| tracedecay_dashboard_api::PrAutoTrackManagedSummaryEntryV1 { + branch: entry.branch, + pr: entry.pr, + head_branch: entry.head_branch, + }, + ) + .collect() + }); + let projected = reader(data_root.path().to_path_buf()); + + assert_eq!(projected.len(), canonical.len()); + for (entry, summary) in projected.iter().zip(canonical.iter()) { + assert_eq!(entry.branch, summary.branch); + assert_eq!(entry.pr, summary.pr); + assert_eq!(entry.head_branch, summary.head_branch); + } + assert_eq!(projected[0].pr, 1); + assert_eq!(projected[1].pr, 3); +} + +#[test] +fn dashboard_managed_summary_reader_is_empty_without_state() { + use std::sync::Arc; + + let data_root = tempfile::tempdir().expect("temp data root"); + let reader: tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader = + Arc::new(|store_root| { + managed_summary(&store_root) + .into_iter() + .map( + |entry| tracedecay_dashboard_api::PrAutoTrackManagedSummaryEntryV1 { + branch: entry.branch, + pr: entry.pr, + head_branch: entry.head_branch, + }, + ) + .collect() + }); + assert!(reader(data_root.path().to_path_buf()).is_empty()); +} diff --git a/crates/tracedecay/src/daemon/project_composition.rs b/crates/tracedecay/src/daemon/project_composition.rs index 4e1c7145fc..c4d933a32b 100644 --- a/crates/tracedecay/src/daemon/project_composition.rs +++ b/crates/tracedecay/src/daemon/project_composition.rs @@ -440,6 +440,7 @@ struct ProjectRoutePorts { tracedecay_dashboard_api::code_index_freshness_api::CodeIndexFreshnessReader, dashboard_explorer_semantic_reader: tracedecay_dashboard_api::ExplorerSemanticReader, dashboard_feedback_status_reader: tracedecay_dashboard_api::feedback_api::FeedbackStatusReader, + dashboard_pr_autotrack_reader: tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader, diagnostic_broker: Arc>, code_index_hook_sink: crate::mcp::server::CodeIndexHookSink, code_index_reconcile_sink: crate::mcp::server::CodeIndexReconcileSink, @@ -493,6 +494,7 @@ impl ComposedCoreServer { .with_dashboard_feedback_status_reader(Arc::clone( &ports.dashboard_feedback_status_reader, )) + .with_dashboard_pr_autotrack_reader(Arc::clone(&ports.dashboard_pr_autotrack_reader)) .with_diagnostics_lsp(Arc::clone(&ports.diagnostic_broker)) .with_code_index_hook_sink(Arc::clone(&ports.code_index_hook_sink)) .with_code_index_reconcile_sink(Arc::clone(&ports.code_index_reconcile_sink)) @@ -884,6 +886,7 @@ impl ProjectOpenInputs<'_> { tracedecay_dashboard_api::feedback_api::feedback_status_reader( self.invocation.feedback_runtime_registrar(), ), + dashboard_pr_autotrack_reader: project_dashboard_pr_autotrack_reader(), diagnostic_broker, code_index_hook_sink, code_index_reconcile_sink, @@ -1942,6 +1945,22 @@ fn project_dashboard_freshness_reader( reader } +fn project_dashboard_pr_autotrack_reader() +-> tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader { + Arc::new(|store_root| { + crate::daemon::pr_autotrack::managed_summary(&store_root) + .into_iter() + .map( + |entry| tracedecay_dashboard_api::PrAutoTrackManagedSummaryEntryV1 { + branch: entry.branch, + pr: entry.pr, + head_branch: entry.head_branch, + }, + ) + .collect() + }) +} + /// Register the project graph and the session databases this route owns with /// the sampling authority. An unavailable registration is recorded and skipped, /// never fatal: telemetry must not fail an otherwise healthy project open. diff --git a/crates/tracedecay/src/mcp/server.rs b/crates/tracedecay/src/mcp/server.rs index f6bd767432..46851f321d 100644 --- a/crates/tracedecay/src/mcp/server.rs +++ b/crates/tracedecay/src/mcp/server.rs @@ -352,6 +352,8 @@ pub struct McpServer { dashboard_explorer_semantic_reader: Option, dashboard_feedback_status_reader: Option, + dashboard_pr_autotrack_reader: + Option, background_refresh_writer: BackgroundRefreshWriter, /// Bridge delivering after-edit hook paths into the daemon-owned code-index /// scheduler queue. `None` for direct servers with no scheduler registry. @@ -847,6 +849,7 @@ impl McpServer { dashboard_code_index_freshness_reader, dashboard_explorer_semantic_reader, dashboard_feedback_status_reader, + dashboard_pr_autotrack_reader, diagnostics_lsp, background_refresh_writer, code_index_hook_sink, @@ -1109,6 +1112,7 @@ impl McpServer { dashboard_code_index_freshness_reader, dashboard_explorer_semantic_reader, dashboard_feedback_status_reader, + dashboard_pr_autotrack_reader, background_refresh_writer, code_index_hook_sink, code_index_reconcile_sink, diff --git a/crates/tracedecay/src/mcp/server/construction.rs b/crates/tracedecay/src/mcp/server/construction.rs index 48b652d047..24642cc329 100644 --- a/crates/tracedecay/src/mcp/server/construction.rs +++ b/crates/tracedecay/src/mcp/server/construction.rs @@ -153,6 +153,8 @@ pub(crate) struct McpServerConstructionContext { Option, pub(crate) dashboard_feedback_status_reader: Option, + pub(crate) dashboard_pr_autotrack_reader: + Option, pub(crate) diagnostics_lsp: Option>>, pub(crate) background_refresh_writer: BackgroundRefreshWriter, @@ -270,6 +272,7 @@ impl McpServerConstructionContext { dashboard_code_index_freshness_reader: None, dashboard_explorer_semantic_reader: None, dashboard_feedback_status_reader: None, + dashboard_pr_autotrack_reader: None, diagnostics_lsp: None, background_refresh_writer: direct_background_refresh_writer(), code_index_hook_sink: None, @@ -375,6 +378,7 @@ impl McpServerConstructionContext { dashboard_code_index_freshness_reader: None, dashboard_explorer_semantic_reader: None, dashboard_feedback_status_reader: None, + dashboard_pr_autotrack_reader: None, diagnostics_lsp: None, background_refresh_writer: writers.background_refresh, code_index_hook_sink: None, @@ -441,6 +445,7 @@ impl McpServerConstructionContext { dashboard_code_index_freshness_reader: None, dashboard_explorer_semantic_reader: None, dashboard_feedback_status_reader: None, + dashboard_pr_autotrack_reader: None, diagnostics_lsp: None, background_refresh_writer: writers.background_refresh, code_index_hook_sink: None, @@ -635,6 +640,14 @@ impl McpServerConstructionContext { self } + pub(crate) fn with_dashboard_pr_autotrack_reader( + mut self, + reader: tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader, + ) -> Self { + self.dashboard_pr_autotrack_reader = Some(reader); + self + } + pub(crate) fn with_diagnostics_lsp( mut self, diagnostics_lsp: Arc< diff --git a/crates/tracedecay/src/mcp/server/requests/tool_dispatch.rs b/crates/tracedecay/src/mcp/server/requests/tool_dispatch.rs index 395807c724..10e515d51b 100644 --- a/crates/tracedecay/src/mcp/server/requests/tool_dispatch.rs +++ b/crates/tracedecay/src/mcp/server/requests/tool_dispatch.rs @@ -306,6 +306,7 @@ impl McpServer { code_index_freshness_reader: self.dashboard_code_index_freshness_reader.clone(), explorer_semantic_reader: self.dashboard_explorer_semantic_reader.clone(), feedback_status_reader: self.dashboard_feedback_status_reader.clone(), + pr_autotrack_reader: self.dashboard_pr_autotrack_reader.clone(), diagnostics_lsp: Some(Arc::clone(&self.diagnostics_lsp)), application_invocation_executor, application_invocation_target, diff --git a/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs b/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs index f7f74670c9..d310004024 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs @@ -672,6 +672,7 @@ pub(super) async fn handle_dashboard( >, explorer_semantic_reader: Option, feedback_status_reader: Option, + pr_autotrack_reader: Option, code_diagnostics_broker: Option< Arc>, >, @@ -937,6 +938,7 @@ pub(super) async fn handle_dashboard( code_index_freshness_reader, explorer_semantic_reader, feedback_status_reader, + pr_autotrack_reader, code_diagnostics_broker, application_invocation_executor, delivery_settlement_authority, diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs index 200b7cbdc7..ab3adb9596 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs @@ -1521,6 +1521,7 @@ fn dispatch_session_workflow_tools_inner<'a>( options.code_index_freshness_reader.clone(), options.explorer_semantic_reader.clone(), options.feedback_status_reader.clone(), + options.pr_autotrack_reader.clone(), options.diagnostics_lsp.clone(), options.dashboard_application_invocation_executor.clone(), options.dashboard_delivery_settlement_authority.clone(), diff --git a/crates/tracedecay/src/mcp/tools/handlers/mod.rs b/crates/tracedecay/src/mcp/tools/handlers/mod.rs index fb2a86e1b7..603220569f 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/mod.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/mod.rs @@ -271,6 +271,8 @@ pub struct ToolCallRegistryOptions<'a> { pub(crate) explorer_semantic_reader: Option, pub feedback_status_reader: Option, + pub(crate) pr_autotrack_reader: + Option, pub diagnostics_lsp: Option>>, pub application_invocation_executor: @@ -349,6 +351,7 @@ impl Default for ToolCallRegistryOptions<'_> { code_index_freshness_reader: None, explorer_semantic_reader: None, feedback_status_reader: None, + pr_autotrack_reader: None, diagnostics_lsp: None, application_invocation_executor: None, dashboard_application_invocation_executor: None,