v0.1.4-pre.010

This commit is contained in:
2026-08-16 15:04:58 +02:00
parent cb10fd354b
commit f0f2954236
17 changed files with 1007 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/constants.rs
// version: 3
// version: 4
//! Application-owned tracing targets and domains.
@@ -19,3 +19,5 @@ pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/errors.rs
// version: 5
// version: 6
//! Application-local error codes for the configuration desktop shell.
@@ -15,6 +15,11 @@ pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing");
/// Config profile data could not be projected safely for the frontend.
pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
/// Splash readiness was invoked from a window other than the splash window.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs
// version: 6
// version: 7
//! Tauri desktop application for managing and validating KSP configuration.
@@ -14,6 +14,7 @@ mod documents;
mod dto_common;
mod errors;
mod frontend_logging;
mod profiles;
mod splash;
mod tauri;
mod tw_main;
@@ -28,6 +29,7 @@ pub(crate) use self::bootstrap::initialize_logging;
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
pub(crate) use self::constants::TRACING_TARGET;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
@@ -45,6 +47,8 @@ pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID;
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING;
pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED;
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
@@ -52,6 +56,8 @@ pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
pub(crate) use self::frontend_logging::emit_frontend_log_event;
pub(crate) use self::profiles::ConfigProfileDetailDto;
pub(crate) use self::profiles::ConfigProfileDocumentDto;
pub(crate) use self::splash::SplashOrderDto;
pub(crate) use self::splash::SplashSettings;
pub(crate) use self::tw_main::show_and_focus as show_main_window;

View File

@@ -0,0 +1,351 @@
// file: crates/ksp-app-config-desk/src/profiles.rs
// version: 1
//! Safe Config profile inspection and provenance projections for Config Desk.
use ts_rs::TS; // rust-rules: derive-import
/// One Config document that exposes the standard `default_profile` / `profiles` contract.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/profiles/ConfigProfileDocumentDto.ts")]
pub(crate) struct ConfigProfileDocumentDto {
/// Stable Config file identifier.
pub(crate) file_id: String,
/// Physical path resolved by Config.
pub(crate) path: String,
/// Autonomous default profile declared by the document.
pub(crate) default_profile: String,
/// Available unique profile identifiers in source order.
pub(crate) profile_ids: std::vec::Vec<String>,
}
/// Top-level origin of one effective profile key.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/profiles/ConfigProfileValueOriginDto.ts")]
pub(crate) struct ConfigProfileValueOriginDto {
/// Effective top-level key.
pub(crate) key: String,
/// `global` or `profile`.
pub(crate) origin: String,
}
/// Safe provenance record for one environment contribution in the resolved effective profile.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/profiles/ConfigProfileEnvironmentProvenanceDto.ts")]
pub(crate) struct ConfigProfileEnvironmentProvenanceDto {
/// RFC 6901 pointer of the resolved value.
pub(crate) json_pointer: String,
/// Referenced environment variable name; never its value.
pub(crate) variable_name: String,
/// Winning source: `process`, `dotenv` or `fallback`.
pub(crate) source: String,
/// Sensitivity derived from the variable namespace.
pub(crate) sensitivity: String,
}
/// Safe detailed view of one resolved Config profile.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/profiles/ConfigProfileDetailDto.ts")]
pub(crate) struct ConfigProfileDetailDto {
/// Stable Config file identifier.
pub(crate) file_id: String,
/// Physical path resolved by Config.
pub(crate) path: String,
/// Autonomous default profile declared by the document.
pub(crate) default_profile: String,
/// Available profile identifiers.
pub(crate) profile_ids: std::vec::Vec<String>,
/// Profile selected for this inspection.
pub(crate) selected_profile: String,
/// `default_profile` or `explicit`.
pub(crate) selection_source: String,
/// Pretty-printed document-global source values.
pub(crate) globals_json: String,
/// Pretty-printed selected source profile object.
pub(crate) profile_json: String,
/// Pretty-printed environment-resolved effective tree using only the safe/redacted representation.
pub(crate) effective_safe_json: String,
/// Strongest sensitivity present in the resolved effective tree.
pub(crate) effective_sensitivity: String,
/// Global/Profile provenance for effective top-level keys.
pub(crate) value_origins: std::vec::Vec<ConfigProfileValueOriginDto>,
/// Environment provenance records without resolved values.
pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>,
}
/// Lists validated Config documents that expose standard profiles.
pub(crate) fn inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
return inventory_from_management(state.config_management());
}
/// Resolves one default or explicitly selected profile into a safe inspection DTO.
pub(crate) fn detail(state: &crate::AppState, file_id: &str, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ConfigProfileDetailDto> {
return detail_from_management(state.config_management(), file_id, requested_profile);
}
fn inventory_from_management(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
let mut result = std::vec::Vec::<ConfigProfileDocumentDto>::new();
for descriptor in management.engine().registry().descriptors() {
if descriptor.kind() != ksp_config_lib::ConfigFileKind::Config {
continue;
}
let document = management.engine().load_validated_document(descriptor.file_id());
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let contract = profile_contract(&document);
let contract = match contract {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => continue,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
result.push(ConfigProfileDocumentDto {
file_id: descriptor.file_id().as_str().to_owned(),
path: document.path().to_string_lossy().into_owned(),
default_profile: contract.default_profile,
profile_ids: contract.profile_ids,
});
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_PROFILES,
profile_document_count = result.len(),
"Config profile document inventory evaluated"
);
return std::result::Result::Ok(result);
}
fn detail_from_management(
management: &ksp_config_lib::ConfigManagement,
file_id: &str,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ConfigProfileDetailDto> {
let file_id = ksp_config_lib::ConfigFileId::new(file_id.to_owned());
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document = management.engine().load_validated_document(&file_id);
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let contract = profile_contract(&document);
let contract = match contract {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_CONTRACT_MISSING, "Config document does not expose the standard profile contract")
.with_context("file_id", file_id.as_str()),
);
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = management.engine().load_resolved_profile(&file_id, requested_profile);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective = resolved.resolve_effective_environment_detailed(&environment);
let effective = match effective {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let globals_json = pretty_json(resolved.globals());
let globals_json = match globals_json {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile_json = pretty_json(resolved.profile());
let profile_json = match profile_json {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_safe_json = serde_json::to_string_pretty(effective.safe_value());
let effective_safe_json = match effective_safe_json {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_PROJECTION_FAILED, "Cannot serialize safe effective Config profile").with_source(error),
);
},
};
let value_origins = value_origins(&resolved);
let environment_provenance = environment_provenance(&effective);
let environment_provenance = match environment_provenance {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let selection_source = selection_source_label(resolved.selection_source()).to_owned();
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_PROFILES,
file_id = file_id.as_str(),
profile_id = resolved.profile_id(),
selection_source = selection_source.as_str(),
effective_sensitivity = sensitivity_label(effective.sensitivity()),
environment_provenance_count = environment_provenance.len(),
"Config profile detail resolved"
);
return std::result::Result::Ok(ConfigProfileDetailDto {
file_id: file_id.as_str().to_owned(),
path: resolved.path().to_string_lossy().into_owned(),
default_profile: contract.default_profile,
profile_ids: contract.profile_ids,
selected_profile: resolved.profile_id().to_owned(),
selection_source,
globals_json,
profile_json,
effective_safe_json,
effective_sensitivity: sensitivity_label(effective.sensitivity()).to_owned(),
value_origins,
environment_provenance,
});
}
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> {
let root = match document.value().as_object() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let default_profile = root.get("default_profile");
let profiles = root.get("profiles");
if default_profile.is_none() && profiles.is_none() {
return std::result::Result::Ok(std::option::Option::None);
}
let default_profile = match default_profile.and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => value.to_owned(),
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_PROJECTION_FAILED, "Validated Config profile document has no readable default_profile")
.with_context("file_id", document.file_id().as_str()),
);
},
};
let profiles = match profiles.and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_PROJECTION_FAILED, "Validated Config profile document has no readable profiles array")
.with_context("file_id", document.file_id().as_str()),
);
},
};
let mut profile_ids = std::vec::Vec::<String>::new();
for profile in profiles {
let profile_id = profile
.as_object()
.and_then(|value| {
return value.get("profile_id");
})
.and_then(serde_json::Value::as_str);
let profile_id = match profile_id {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_PROJECTION_FAILED, "Validated Config profile entry has no readable profile_id")
.with_context("file_id", document.file_id().as_str()),
);
},
};
profile_ids.push(profile_id.to_owned());
}
return std::result::Result::Ok(std::option::Option::Some(ProfileContract { default_profile, profile_ids }));
}
fn pretty_json(value: &serde_json::Map<String, serde_json::Value>) -> ksp_core_lib::Result<String> {
let serialized = serde_json::to_string_pretty(value);
return match serialized {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_PROJECTION_FAILED, "Cannot serialize Config profile source projection").with_source(error),
),
};
}
fn value_origins(resolved: &ksp_config_lib::ResolvedConfigProfile) -> std::vec::Vec<ConfigProfileValueOriginDto> {
let mut result = std::vec::Vec::<ConfigProfileValueOriginDto>::new();
for key in resolved.effective().keys() {
let origin = resolved.origin(key.as_str());
let origin = match origin {
std::option::Option::Some(ksp_config_lib::ConfigValueOrigin::Global) => "global",
std::option::Option::Some(ksp_config_lib::ConfigValueOrigin::Profile) => "profile",
std::option::Option::None => "unknown",
};
result.push(ConfigProfileValueOriginDto { key: key.clone(), origin: origin.to_owned() });
}
return result;
}
fn environment_provenance(effective: &ksp_config_lib::ResolvedConfigJson) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>> {
let mut result = std::vec::Vec::<ConfigProfileEnvironmentProvenanceDto>::new();
for (json_pointer, provenance) in effective.provenance() {
for segment in provenance {
let variable_name = segment.variable_name();
let variable_name = match variable_name {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
let source = segment.environment_source();
let source = match source {
std::option::Option::Some(value) => environment_source_label(value),
std::option::Option::None => continue,
};
let sensitivity = ksp_config_lib::ConfigSensitivity::from_variable_name(variable_name);
let sensitivity = match sensitivity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
result.push(ConfigProfileEnvironmentProvenanceDto {
json_pointer: json_pointer.clone(),
variable_name: variable_name.to_owned(),
source: source.to_owned(),
sensitivity: sensitivity_label(sensitivity).to_owned(),
});
}
}
return std::result::Result::Ok(result);
}
const fn selection_source_label(source: ksp_config_lib::ConfigProfileSelectionSource) -> &'static str {
return match source {
ksp_config_lib::ConfigProfileSelectionSource::DefaultProfile => "default_profile",
ksp_config_lib::ConfigProfileSelectionSource::Explicit => "explicit",
ksp_config_lib::ConfigProfileSelectionSource::Composite => "composite",
};
}
const fn environment_source_label(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
const fn sensitivity_label(sensitivity: ksp_config_lib::ConfigSensitivity) -> &'static str {
return match sensitivity {
ksp_config_lib::ConfigSensitivity::Public => "public",
ksp_config_lib::ConfigSensitivity::Internal => "internal",
ksp_config_lib::ConfigSensitivity::Secret => "secret",
};
}
#[cfg(test)]
#[path = "../unit_tests/profiles.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs
// version: 7
// version: 8
//! Tauri runtime assembly for the KSP configuration desktop application.
@@ -42,6 +42,8 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
get_config_documents,
get_config_document_detail,
save_config_document_source,
get_config_profile_documents,
get_config_profile_detail,
emit_frontend_log,
splash_frontend_ready
]);
@@ -92,6 +94,30 @@ fn save_config_document_source(
return crate::documents::save_source(&state, file_id.as_str(), source.as_str());
}
#[tauri::command]
fn get_config_profile_documents(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> {
let result = crate::profiles::inventory(&state);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn get_config_profile_detail(
file_id: String,
profile_id: std::option::Option<String>,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> {
let result = crate::profiles::detail(&state, file_id.as_str(), profile_id.as_deref());
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);