Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/tracedecay-contracts/src/doctor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ pub use sources::{
RemoteAuthorityReadV1, RemoteListenerReadV1, RemoteOperationalReadV1, ResidentMemoryDoctorPort,
ResidentMemoryHolderReadV1, ResidentMemoryOwnerReadV1, ResidentMemoryReadV1,
RuntimeHealthDoctorPort, RuntimeHealthReadV1, RuntimeLivenessV1, StorageDoctorPort,
advisory_feedback_findings, code_index_finding, configuration_finding, github_source_finding,
host_integration_finding, ingest_refusal_finding, language_server_finding,
observability_finding, operational_audit_findings, resident_memory_findings,
runtime_health_finding,
UnappliedConfigurationSettingV1, advisory_feedback_findings, code_index_finding,
configuration_finding, github_source_finding, host_integration_finding, ingest_refusal_finding,
language_server_finding, observability_finding, operational_audit_findings,
resident_memory_findings, runtime_health_finding,
};
pub use types::{
DoctorCoverageCompletenessV1, DoctorCoverageStatementV1, DoctorEvidenceRefV1,
Expand Down
78 changes: 55 additions & 23 deletions crates/tracedecay-contracts/src/doctor/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ use std::pin::Pin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tracedecay_domain::configuration::SettingKey;
use tracedecay_domain::{
CodeGenerationId, FeedbackCycleId, FeedbackCycleTerminationV1, FeedbackFindingId,
FeedbackFindingLifecycleV1, FeedbackResultId, FeedbackScopeV1, ProviderEvaluationStateV1,
RetrievalAnchorId,
};

use crate::RequestContext;
use crate::configuration::ConfigurationSettingFindingV1;
use crate::error::ApplicationContractError;
use crate::storage::findings::truncate_at_char_boundary;

Expand Down Expand Up @@ -123,7 +125,7 @@ fn clean_finding(
}

/// The observed drift between desired and effective configuration.
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ConfigurationDriftV1 {
/// Desired and effective configuration agree.
Expand All @@ -132,6 +134,32 @@ pub enum ConfigurationDriftV1 {
Drifted,
/// A requested pin could not be honored by the authority.
PinUnavailable,
/// The resolved configuration stores values the effective configuration
/// leaves unapplied.
Unapplied(Vec<UnappliedConfigurationSettingV1>),
}

/// One stored setting value the effective configuration does not apply.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct UnappliedConfigurationSettingV1 {
pub key: SettingKey,
pub finding: ConfigurationSettingFindingV1,
}

fn unapplied_statement(settings: &[UnappliedConfigurationSettingV1]) -> String {
let statements = settings.iter().map(|setting| {
let key = setting.key.as_str();
match &setting.finding {
ConfigurationSettingFindingV1::InvalidIndexPathPattern {
pattern, message, ..
} => format!(
"{key} stores pattern {pattern:?} that does not compile ({message}); indexing \
runs without it. Set {key} to patterns that compile or unset it \
(tracedecay_configuration_set / tracedecay_configuration_unset)"
),
}
});
bounded_statement(&statements.collect::<Vec<_>>().join("; "))
}

/// One configuration-authority resolve/pin health read.
Expand Down Expand Up @@ -160,28 +188,32 @@ pub fn configuration_finding(
) -> Result<DoctorFindingV1, ApplicationContractError> {
let family = DoctorFindingFamilyV1::Configuration;
match read {
ConfigurationAuthorityReadV1::Resolved { drift, coverage } => match drift {
ConfigurationDriftV1::InSync => clean_finding(
family,
"configuration.resolved.in-sync",
*coverage,
"effective configuration matches the resolved authority",
),
ConfigurationDriftV1::Drifted => source_finding(
family,
DoctorEvidenceStateV1::Degraded,
"configuration.resolved.drifted",
*coverage,
"effective configuration diverges from the desired authority",
),
ConfigurationDriftV1::PinUnavailable => source_finding(
family,
DoctorEvidenceStateV1::Degraded,
"configuration.resolved.pin-unavailable",
*coverage,
"a requested configuration pin could not be honored",
),
},
ConfigurationAuthorityReadV1::Resolved { drift, coverage } => {
let (reference, statement) = match drift {
ConfigurationDriftV1::InSync => {
return clean_finding(
family,
"configuration.resolved.in-sync",
*coverage,
"effective configuration matches the resolved authority",
);
}
ConfigurationDriftV1::Drifted => (
"configuration.resolved.drifted",
Cow::Borrowed("effective configuration diverges from the desired authority"),
),
ConfigurationDriftV1::PinUnavailable => (
"configuration.resolved.pin-unavailable",
Cow::Borrowed("a requested configuration pin could not be honored"),
),
ConfigurationDriftV1::Unapplied(settings) => (
"configuration.resolved.unapplied",
Cow::Owned(unapplied_statement(settings)),
),
};
let state = DoctorEvidenceStateV1::Degraded;
source_finding(family, state, reference, *coverage, &statement)
}
ConfigurationAuthorityReadV1::Unsupported => unobservable_finding(
family,
DoctorEvidenceStateV1::Unsupported,
Expand Down
43 changes: 30 additions & 13 deletions crates/tracedecay-daemon-service/src/doctor_kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::sync::Arc;
use tracedecay_application::advisory::github_runtime::github_source_status_v1;
use tracedecay_code_index_runtime::code_index_scheduler::CodeIndexSchedulerRegistryV1;
use tracedecay_code_index_runtime::code_index_scheduler::identity::repository_id_for;
use tracedecay_configuration::config::PinnedRuntimeConfiguration;
use tracedecay_configuration::config::{PinnedRuntimeConfiguration, setting_findings};
use tracedecay_contracts::doctor::{
AdvisoryFeedbackDoctorPort, AdvisoryFeedbackReadV1, CodeIndexMountDoctorPort,
CodeIndexMountReadV1, CodeIndexMountStateV1, ConfigurationAuthorityDoctorPort,
Expand All @@ -27,8 +27,8 @@ use tracedecay_contracts::doctor::{
OperationalAuditDoctorPort, OperationalAuditReadV1, ProfileAuthorityReadV1,
RemoteOperationalReadV1, ResidentMemoryDoctorPort, ResidentMemoryHolderReadV1,
ResidentMemoryOwnerReadV1, ResidentMemoryReadV1, RuntimeHealthDoctorPort, RuntimeHealthReadV1,
StorageDoctorPort, advisory_feedback_read_from_publication, merge_storage_reads,
runtime_health_read, storage_family_read,
StorageDoctorPort, UnappliedConfigurationSettingV1, advisory_feedback_read_from_publication,
merge_storage_reads, runtime_health_read, storage_family_read,
};
use tracedecay_contracts::request_identity::{GlobalRequestSurface, mint_global_request_id};
use tracedecay_contracts::storage::SchemaConvergenceFindingV1;
Expand All @@ -51,21 +51,38 @@ const DOCTOR_CONTEXT_HORIZON_MICROS: i64 = 30_000_000;

/// Map a real pinned-configuration lookup outcome into a kernel read.
///
/// A pinned snapshot resolves in-sync (the cache invariant guarantees the pinned
/// configuration equals the value derived from its resolved snapshot, so within
/// the cache there is no unobserved drift). A cold cache, the fail-closed
/// accessor's `Err`, is a typed [`ConfigurationAuthorityReadV1::Absent`], never
/// a fabricated healthy result.
/// A pinned snapshot is in sync only when the runtime configuration applies
/// every stored value; each part [`setting_findings`] reports it leaves
/// unapplied makes the read [`ConfigurationDriftV1::Unapplied`]. A cold cache,
/// the fail-closed accessor's `Err`, is a typed
/// [`ConfigurationAuthorityReadV1::Absent`], never a fabricated healthy result.
#[must_use]
pub fn configuration_read_from_pin<E>(
resolved: &Result<PinnedRuntimeConfiguration, E>,
) -> ConfigurationAuthorityReadV1 {
match resolved {
Ok(_) => ConfigurationAuthorityReadV1::Resolved {
drift: ConfigurationDriftV1::InSync,
coverage: DoctorCoverageCompletenessV1::Complete,
let Ok(pinned) = resolved else {
return ConfigurationAuthorityReadV1::Absent;
};
let unapplied: Vec<_> = pinned
.snapshot()
.effective_values
.iter()
.flat_map(|(key, value)| {
setting_findings(key, value).into_iter().map(|finding| {
UnappliedConfigurationSettingV1 {
key: key.clone(),
finding,
}
})
})
.collect();
ConfigurationAuthorityReadV1::Resolved {
drift: if unapplied.is_empty() {
ConfigurationDriftV1::InSync
} else {
ConfigurationDriftV1::Unapplied(unapplied)
},
Err(_) => ConfigurationAuthorityReadV1::Absent,
coverage: DoctorCoverageCompletenessV1::Complete,
}
}

Expand Down
82 changes: 82 additions & 0 deletions crates/tracedecay-daemon-service/src/doctor_kernel/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,88 @@ fn configuration_read_from_pin_absent_on_cold_cache() {
);
}

fn pinned_index_exclude(
patterns: &[&str],
) -> tracedecay_configuration::config::PinnedRuntimeConfiguration {
use tracedecay_configuration::config::registry::ConfigurationRegistry;
use tracedecay_configuration::config::resolver::{ConfigurationLayerV1, resolve_configuration};
use tracedecay_domain::configuration::{
ConfigurationLayerIdV1, ConfigurationRevisionId, ConfigurationValueV1,
INDEX_EXCLUDE_SETTING_KEY, SettingKey,
};

let project_id = tracedecay_domain::ProjectId::new("project.doctor-unapplied-exclude").unwrap();
let revision = ConfigurationRevisionId::new("configuration.revision.doctor-exclude").unwrap();
let snapshot = resolve_configuration(
&ConfigurationRegistry::core().unwrap(),
&[ConfigurationLayerV1 {
layer: ConfigurationLayerIdV1::Project {
project_id: project_id.clone(),
},
revision_id: revision.clone(),
entries: std::collections::BTreeMap::from([(
SettingKey::new(INDEX_EXCLUDE_SETTING_KEY).unwrap(),
ConfigurationValueV1::StringList(
patterns
.iter()
.map(|pattern| (*pattern).to_owned())
.collect(),
),
)]),
}],
)
.expect("read-time validation admits a stored pattern")
.snapshot;
tracedecay_configuration::config::PinnedRuntimeConfiguration::new(
tracedecay_configuration::config::RuntimeConfigurationTarget {
profile_root: std::path::PathBuf::from("/profile"),
project_id,
project_root: std::path::PathBuf::from("/project"),
},
revision,
snapshot,
)
.expect("an unapplied pattern must not refuse the pin")
}

/// A stored exclude pattern the pin skips is configuration out of sync with
/// what the operator stored, never "matches the resolved authority".
#[test]
fn configuration_finding_reports_a_stored_pattern_the_pin_leaves_unapplied() {
use tracedecay_contracts::doctor::{DoctorEvidenceStateV1, configuration_finding};

let finding = |patterns: &[&str]| {
let read = configuration_read_from_pin::<&str>(&Ok(pinned_index_exclude(patterns)));
let finding = configuration_finding(&read).expect("configuration finding");
(
finding.state(),
finding.evidence()[0].reference().as_str().to_owned(),
finding.coverage().statement().to_owned(),
)
};

assert_eq!(
finding(&["src/[abc", "docs/**"]),
(
DoctorEvidenceStateV1::Degraded,
"configuration.resolved.unapplied".to_owned(),
"index.exclude.v1 stores pattern \"src/[abc\" that does not compile (unclosed \
character class; missing ']'); indexing runs without it. Set index.exclude.v1 to \
patterns that compile or unset it (tracedecay_configuration_set / \
tracedecay_configuration_unset)"
.to_owned(),
)
);
assert_eq!(
finding(&["docs/**"]),
(
DoctorEvidenceStateV1::HealthyCompleteCoverage,
"configuration.resolved.in-sync".to_owned(),
"effective configuration matches the resolved authority".to_owned(),
)
);
}

/// The daemon-side Doctor reader must observe the exhaustive
/// observation-authority invariant pass itself. Without a producer the signal
/// is permanently not-run, which downgrades every `StorageRuntime` finding to
Expand Down
53 changes: 5 additions & 48 deletions crates/tracedecay/src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ use tracedecay_contracts::project_open::{
use tracedecay_contracts::storage::{
SchemaConvergenceFindingV1, SchemaConvergenceProgressV1, SchemaConvergenceStateV1,
};
use tracedecay_contracts::{ApplicationOutcome, ConfigurationSettingFindingV1, ResolvedSetting};
use tracedecay_contracts::{ApplicationOutcome, ResolvedSetting};
use tracedecay_domain::configuration::{
ConfigurationValueV1, INDEX_EXCLUDE_SETTING_KEY, INDEX_INCLUDE_SETTING_KEY, SettingKey,
USER_UPLOAD_ENABLED_SETTING_KEY,
ConfigurationValueV1, SettingKey, USER_UPLOAD_ENABLED_SETTING_KEY,
};
use tracedecay_tool_catalog::{ApplicationSurfaceOperation, BindingSurface};

Expand Down Expand Up @@ -156,9 +155,6 @@ pub async fn run_doctor(
daemon_status.as_ref(),
&mut pending_reset,
)?;
if matches!(daemon_status, Some(Ok(Some(_)))) {
check_project_index_paths(&mut dc, profile, &project_path).await;
}
check_watcher(&mut dc, profile);
let upload_enabled = if daemon_listening {
configured_upload_enabled(profile)
Expand Down Expand Up @@ -1063,8 +1059,7 @@ fn check_automation_effect_resets(
async fn configured_upload_enabled(
profile: &tracedecay_runtime_core::config::ProfileRoot,
) -> tracedecay_domain::errors::Result<bool> {
// The setting belongs to the profile: the request names no project.
let setting = configured_setting(profile, None, USER_UPLOAD_ENABLED_SETTING_KEY).await?;
let setting = configured_setting(profile, USER_UPLOAD_ENABLED_SETTING_KEY).await?;
match setting.effective_value {
ConfigurationValueV1::Boolean(enabled) => Ok(enabled),
_ => Err(tracedecay_domain::errors::TraceDecayError::Config {
Expand All @@ -1073,41 +1068,8 @@ async fn configured_upload_enabled(
}
}

/// Reports every stored index path pattern the project's runtime
/// configuration leaves unapplied, with the command that repairs it.
#[hotpath::measure(label = "doctor.config.index_paths", future = true)]
async fn check_project_index_paths(
dc: &mut DoctorCounters,
profile: &tracedecay_runtime_core::config::ProfileRoot,
project_path: &Path,
) {
for key in [INDEX_EXCLUDE_SETTING_KEY, INDEX_INCLUDE_SETTING_KEY] {
match configured_setting(profile, Some(project_path), key).await {
Ok(setting) => {
for finding in setting.findings {
match finding {
ConfigurationSettingFindingV1::InvalidIndexPathPattern {
pattern,
message,
..
} => dc.fail(&format!(
"{key} stores pattern {pattern:?} that does not compile ({message}); \
indexing runs without it. Set {key} to patterns that compile or \
unset it (tracedecay_configuration_set / tracedecay_configuration_unset)"
)),
}
}
}
// A reset-required or still-mounting store is already reported
// as its own state; a read that could not run is not an issue.
Err(error) => dc.warn(&format!("{key} could not be read: {error}")),
}
}
}

async fn configured_setting(
profile: &tracedecay_runtime_core::config::ProfileRoot,
project_path: Option<&Path>,
key: &str,
) -> tracedecay_domain::errors::Result<ResolvedSetting> {
let operation = ApplicationSurfaceOperation::ConfigurationGet;
Expand All @@ -1122,13 +1084,8 @@ async fn configured_setting(
message: format!("could not create Doctor configuration request: {error}"),
}
})?;
let handshake = crate::daemon::handshake_for_current_client(
profile,
project_path.map(Path::to_path_buf),
None,
false,
false,
)?;
// The setting belongs to the profile: the request names no project.
let handshake = crate::daemon::handshake_for_current_client(profile, None, None, false, false)?;
let client = crate::daemon::invocation_client_for_current(profile, handshake)?;
let dispatched = resolve_application_surface_dispatch(
BindingSurface::Cli,
Expand Down
Loading