use std::collections::BTreeMap;
use std::fmt;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use serde::Serialize;
use toml_edit::{DocumentMut, Item};
use crate::config::{
ConfigFile, ConfigLayers, ConfigShowEntry, ConfigShowReport, EnvironmentConfigError,
GRAPH_CAUSAL_MIN_COST_NORMALIZATION_KEY, GRAPH_CURATE_ARTICULATION_PROTECTION_MULTIPLIER_KEY,
GRAPH_CURATE_ONION_DECAY_MAX_KEY, GRAPH_FEATURE_CAUSAL_EXPLAIN_ENABLED_KEY,
GRAPH_FEATURE_HITS_PROFILES_ENABLED_KEY, GRAPH_FEATURE_LOAD_BEARING_ENABLED_KEY,
GRAPH_FEATURE_PACK_DNA_ENABLED_KEY, GRAPH_FEATURE_PPR_ENABLED_KEY,
GRAPH_FEATURE_PROXIMITY_ENABLED_KEY, GRAPH_FEATURE_REVISION_DOMINANCE_ENABLED_KEY,
GRAPH_FEATURE_SKYLINE_ENABLED_KEY, GRAPH_FEATURE_STRUCTURAL_DECAY_ENABLED_KEY,
GRAPH_FEATURE_STRUCTURAL_HEALTH_ENABLED_KEY, GRAPH_GOMORY_HU_SAMPLE_SIZE_KEY,
GRAPH_GOMORY_HU_SAMPLE_THRESHOLD_KEY, GRAPH_HEALTH_CONTRADICTION_THRESHOLD_KEY,
GRAPH_HITS_PROFILE_BOOST_KEY, GRAPH_MEMORY_DEGRADED_BELOW_PCT_KEY,
GRAPH_MEMORY_GROWTH_MULTIPLIER_BASIS_POINTS_KEY, GRAPH_MEMORY_PER_ALGORITHM_CAP_MB_KEY,
GRAPH_MEMORY_SNAPSHOT_CAP_MB_KEY, GRAPH_PACK_DNA_MAX_EDGES_KEY, GRAPH_PACK_DNA_MAX_ITEMS_KEY,
GRAPH_PPR_ALPHA_KEY, GRAPH_WITNESSES_ALGORITHM_TTL_DAYS_KEY,
GRAPH_WITNESSES_RETENTION_DAYS_KEY, MEMORY_INCLUDE_GLOBAL_KEY, MEMORY_PARTICIPATE_KEY,
PathExpander, SEARCH_DEFAULT_SPEED_KEY, SEARCH_GRAPH_WEIGHT_KEY, SEARCH_LEXICAL_WEIGHT_KEY,
SEARCH_RERANK_KEY, SEARCH_RERANK_TOP_K_KEY, SEARCH_SEMANTIC_WEIGHT_KEY, built_in_config,
config_from_env, merge_config,
};
pub const CONFIG_GET_SCHEMA_V1: &str = "ee.config.get.v1";
pub const CONFIG_SET_SCHEMA_V1: &str = "ee.config.set.v1";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigSurfaceOptions {
pub workspace_root: PathBuf,
pub config_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigGetReport {
pub schema: &'static str,
pub key: &'static str,
pub value: String,
pub source: &'static str,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigSetReport {
pub schema: &'static str,
pub command: &'static str,
pub dry_run: bool,
pub key: &'static str,
pub value: String,
pub before: Option<String>,
pub config_path: String,
pub path_redaction: &'static str,
pub config_exists: bool,
pub would_write: bool,
pub applied: bool,
pub repair: Option<&'static str>,
pub planned_toml: String,
}
#[derive(Debug)]
pub enum ConfigSurfaceError {
UnknownKey {
key: String,
},
InvalidPattern {
pattern: String,
},
InvalidValue {
key: &'static str,
value: String,
expected: &'static str,
},
Environment {
source: EnvironmentConfigError,
},
Read {
path: PathBuf,
source: io::Error,
},
Parse {
path: PathBuf,
message: String,
},
Write {
path: PathBuf,
source: io::Error,
},
}
impl fmt::Display for ConfigSurfaceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownKey { key } => write!(formatter, "unknown config key `{key}`"),
Self::InvalidPattern { pattern } => {
write!(formatter, "unsupported config pattern `{pattern}`")
}
Self::InvalidValue {
key,
value,
expected,
} => write!(
formatter,
"invalid value `{value}` for `{key}`; expected {expected}"
),
Self::Environment { source } => write!(formatter, "could not load config: {source}"),
Self::Read { path, source } => {
write!(
formatter,
"could not read config `{}`: {source}",
path.display()
)
}
Self::Parse { path, message } => {
write!(
formatter,
"could not parse config `{}`: {message}",
path.display()
)
}
Self::Write { path, source } => {
write!(
formatter,
"could not write config `{}`: {source}",
path.display()
)
}
}
}
}
impl std::error::Error for ConfigSurfaceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Environment { source } => Some(source),
Self::Read { source, .. } | Self::Write { source, .. } => Some(source),
Self::UnknownKey { .. }
| Self::InvalidPattern { .. }
| Self::InvalidValue { .. }
| Self::Parse { .. } => None,
}
}
}
#[must_use]
pub fn graph_config_keys() -> &'static [&'static str] {
&[
GRAPH_PPR_ALPHA_KEY,
GRAPH_HEALTH_CONTRADICTION_THRESHOLD_KEY,
GRAPH_CURATE_ONION_DECAY_MAX_KEY,
GRAPH_CURATE_ARTICULATION_PROTECTION_MULTIPLIER_KEY,
GRAPH_HITS_PROFILE_BOOST_KEY,
GRAPH_CAUSAL_MIN_COST_NORMALIZATION_KEY,
GRAPH_PACK_DNA_MAX_ITEMS_KEY,
GRAPH_PACK_DNA_MAX_EDGES_KEY,
GRAPH_GOMORY_HU_SAMPLE_THRESHOLD_KEY,
GRAPH_GOMORY_HU_SAMPLE_SIZE_KEY,
GRAPH_MEMORY_SNAPSHOT_CAP_MB_KEY,
GRAPH_MEMORY_PER_ALGORITHM_CAP_MB_KEY,
GRAPH_MEMORY_DEGRADED_BELOW_PCT_KEY,
GRAPH_MEMORY_GROWTH_MULTIPLIER_BASIS_POINTS_KEY,
GRAPH_WITNESSES_RETENTION_DAYS_KEY,
GRAPH_WITNESSES_ALGORITHM_TTL_DAYS_KEY,
GRAPH_FEATURE_PPR_ENABLED_KEY,
GRAPH_FEATURE_PACK_DNA_ENABLED_KEY,
GRAPH_FEATURE_CAUSAL_EXPLAIN_ENABLED_KEY,
GRAPH_FEATURE_STRUCTURAL_HEALTH_ENABLED_KEY,
GRAPH_FEATURE_STRUCTURAL_DECAY_ENABLED_KEY,
GRAPH_FEATURE_PROXIMITY_ENABLED_KEY,
GRAPH_FEATURE_REVISION_DOMINANCE_ENABLED_KEY,
GRAPH_FEATURE_SKYLINE_ENABLED_KEY,
GRAPH_FEATURE_LOAD_BEARING_ENABLED_KEY,
GRAPH_FEATURE_HITS_PROFILES_ENABLED_KEY,
]
}
pub fn show_config(
options: &ConfigSurfaceOptions,
pattern: Option<&str>,
) -> Result<ConfigShowReport, ConfigSurfaceError> {
let mut report = merged_config(options)?.to_show_report();
if let Some(pattern) = pattern {
report.entries = filter_entries(report.entries, pattern)?;
report.entry_count = report.entries.len();
}
Ok(report)
}
pub fn get_config(
options: &ConfigSurfaceOptions,
key: &str,
) -> Result<ConfigGetReport, ConfigSurfaceError> {
let spec = config_key_spec(key).ok_or_else(|| ConfigSurfaceError::UnknownKey {
key: key.to_owned(),
})?;
let report = show_config(options, Some(spec.key))?;
let entry =
report
.entries
.into_iter()
.next()
.ok_or_else(|| ConfigSurfaceError::UnknownKey {
key: key.to_owned(),
})?;
Ok(ConfigGetReport {
schema: CONFIG_GET_SCHEMA_V1,
key: entry.key,
value: entry.value,
source: entry.source,
})
}
pub fn set_config(
options: &ConfigSurfaceOptions,
key: &str,
value: &str,
dry_run: bool,
) -> Result<ConfigSetReport, ConfigSurfaceError> {
let spec = config_key_spec(key).ok_or_else(|| ConfigSurfaceError::UnknownKey {
key: key.to_owned(),
})?;
let scalar = parse_graph_value(spec, value)?;
let path = effective_config_path(&options.workspace_root, options.config_path.as_deref());
let (config_exists, input) = read_optional_config(&path)?;
let mut document =
input
.parse::<DocumentMut>()
.map_err(|source| ConfigSurfaceError::Parse {
path: path.clone(),
message: source.to_string(),
})?;
let before = item_for_path(&document, spec.path).map(item_value_for_report);
let after = scalar.report_value();
set_toml_value(&mut document, spec.path, scalar);
let planned_toml = document.to_string();
ConfigFile::parse(&planned_toml).map_err(|source| ConfigSurfaceError::Parse {
path: path.clone(),
message: source.to_string(),
})?;
let would_write = before.as_deref() != Some(after.as_str());
if !dry_run && would_write {
ensure_no_config_symlink_components(&path, "write").map_err(|source| {
ConfigSurfaceError::Write {
path: path.clone(),
source,
}
})?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|source| ConfigSurfaceError::Write {
path: path.clone(),
source,
})?;
}
ensure_no_config_symlink_components(&path, "write").map_err(|source| {
ConfigSurfaceError::Write {
path: path.clone(),
source,
}
})?;
ensure_config_write_path_is_regular_or_missing(&path).map_err(|source| {
ConfigSurfaceError::Write {
path: path.clone(),
source,
}
})?;
let mut temp_path = path.clone();
temp_path.set_extension("tmp");
ensure_no_config_symlink_components(&temp_path, "write temp").map_err(|source| {
ConfigSurfaceError::Write {
path: temp_path.clone(),
source,
}
})?;
ensure_config_temp_path_is_missing(&temp_path).map_err(|source| {
ConfigSurfaceError::Write {
path: temp_path.clone(),
source,
}
})?;
{
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&temp_path)
.map_err(|source| ConfigSurfaceError::Write {
path: temp_path.clone(),
source,
})?;
file.write_all(planned_toml.as_bytes()).map_err(|source| {
ConfigSurfaceError::Write {
path: temp_path.clone(),
source,
}
})?;
file.sync_data()
.map_err(|source| ConfigSurfaceError::Write {
path: temp_path.clone(),
source,
})?;
}
publish_config_temp_file(&path, &temp_path)?;
}
Ok(ConfigSetReport {
schema: CONFIG_SET_SCHEMA_V1,
command: "config set",
dry_run,
key: spec.key,
value: after,
before,
config_path: path.display().to_string(),
path_redaction: "operator_requested_config_path",
config_exists,
would_write,
applied: !dry_run && would_write,
repair: if dry_run && would_write {
Some("Rerun without `--dry-run` to write .ee/config.toml.")
} else {
None
},
planned_toml,
})
}
fn merged_config(
options: &ConfigSurfaceOptions,
) -> Result<crate::config::MergedConfig, ConfigSurfaceError> {
let environment = process_env();
merged_config_with_environment(options, &environment)
}
fn merged_config_with_environment(
options: &ConfigSurfaceOptions,
process_environment: &BTreeMap<String, std::ffi::OsString>,
) -> Result<crate::config::MergedConfig, ConfigSurfaceError> {
let home_variable = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
let home = process_environment.get(home_variable).map(PathBuf::from);
let expander = PathExpander::with_env(home, process_environment.clone());
let defaults =
built_in_config(&expander).map_err(|source| ConfigSurfaceError::Environment { source })?;
let environment = config_from_env(process_environment, &expander)
.map_err(|source| ConfigSurfaceError::Environment { source })?;
let project = read_project_config(options, &expander)?.unwrap_or_else(ConfigFile::default);
let user = read_user_config(&expander)?.unwrap_or_else(ConfigFile::default);
let mut layers = ConfigLayers::with_defaults(defaults);
layers.environment = environment;
layers.project = project;
layers.user = user;
Ok(merge_config(&layers))
}
pub fn merged_workspace_config(
workspace_root: &Path,
) -> Result<crate::config::MergedConfig, ConfigSurfaceError> {
merged_config(&ConfigSurfaceOptions {
workspace_root: workspace_root.to_path_buf(),
config_path: None,
})
}
fn read_project_config(
options: &ConfigSurfaceOptions,
expander: &PathExpander,
) -> Result<Option<ConfigFile>, ConfigSurfaceError> {
let path = effective_config_path(&options.workspace_root, options.config_path.as_deref());
match read_optional_config_contents(&path)? {
Some(contents) => ConfigFile::parse_with_expander(&contents, expander)
.map(Some)
.map_err(|source| ConfigSurfaceError::Parse {
path,
message: source.to_string(),
}),
None => Ok(None),
}
}
fn read_user_config(expander: &PathExpander) -> Result<Option<ConfigFile>, ConfigSurfaceError> {
let path = expander
.expand("~/.config/ee/config.toml")
.map_err(|source| ConfigSurfaceError::Environment {
source: EnvironmentConfigError::PathExpansion {
variable: "HOME",
source,
},
})?;
match read_optional_config_contents(&path)? {
Some(contents) => ConfigFile::parse_with_expander(&contents, expander)
.map(Some)
.map_err(|source| ConfigSurfaceError::Parse {
path,
message: source.to_string(),
}),
None => Ok(None),
}
}
fn process_env() -> BTreeMap<String, std::ffi::OsString> {
std::env::vars_os()
.filter_map(|(key, value)| key.into_string().ok().map(|key| (key, value)))
.collect()
}
fn filter_entries(
entries: Vec<ConfigShowEntry>,
pattern: &str,
) -> Result<Vec<ConfigShowEntry>, ConfigSurfaceError> {
if pattern == "*" {
return Ok(entries);
}
if let Some(prefix) = pattern.strip_suffix(".*") {
if prefix.is_empty() {
return Err(ConfigSurfaceError::InvalidPattern {
pattern: pattern.to_owned(),
});
}
let dotted_prefix = format!("{prefix}.");
return Ok(entries
.into_iter()
.filter(|entry| entry.key.starts_with(&dotted_prefix))
.collect());
}
if config_key_spec(pattern).is_some() {
return Ok(entries
.into_iter()
.filter(|entry| entry.key == pattern)
.collect());
}
Err(ConfigSurfaceError::InvalidPattern {
pattern: pattern.to_owned(),
})
}
fn effective_config_path(workspace_root: &Path, config_path: Option<&Path>) -> PathBuf {
match config_path {
Some(path) if path.is_absolute() => path.to_path_buf(),
Some(path) => workspace_root.join(path),
None => workspace_root.join(".ee").join("config.toml"),
}
}
fn read_optional_config(path: &Path) -> Result<(bool, String), ConfigSurfaceError> {
match read_optional_config_contents(path)? {
Some(contents) => Ok((true, contents)),
None => Ok((false, String::new())),
}
}
const CONFIG_SURFACE_MAX_BYTES: u64 = 4 * 1024 * 1024;
fn read_optional_config_contents(path: &Path) -> Result<Option<String>, ConfigSurfaceError> {
use std::io::Read as _;
ensure_no_config_symlink_components(path, "read").map_err(|source| {
ConfigSurfaceError::Read {
path: path.to_path_buf(),
source,
}
})?;
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_file() => metadata,
Ok(_) => {
return Err(ConfigSurfaceError::Read {
path: path.to_path_buf(),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"config path is not a regular file",
),
});
}
Err(source)
if matches!(
source.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
return Ok(None);
}
Err(source) => {
return Err(ConfigSurfaceError::Read {
path: path.to_path_buf(),
source,
});
}
};
if metadata.len() > CONFIG_SURFACE_MAX_BYTES {
return Err(ConfigSurfaceError::Read {
path: path.to_path_buf(),
source: io::Error::new(
io::ErrorKind::InvalidData,
format!(
"refusing to read config `{}`: file is {} bytes, exceeding the {CONFIG_SURFACE_MAX_BYTES}-byte ceiling",
path.display(),
metadata.len(),
),
),
});
}
let file = open_config_surface_file_for_read_no_follow(path).map_err(|source| {
ConfigSurfaceError::Read {
path: path.to_path_buf(),
source,
}
})?;
let opened_metadata = file.metadata().map_err(|source| ConfigSurfaceError::Read {
path: path.to_path_buf(),
source,
})?;
if !opened_metadata.file_type().is_file() {
return Err(ConfigSurfaceError::Read {
path: path.to_path_buf(),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"config path is not a regular file after open",
),
});
}
if opened_metadata.len() > CONFIG_SURFACE_MAX_BYTES {
return Err(ConfigSurfaceError::Read {
path: path.to_path_buf(),
source: io::Error::new(
io::ErrorKind::InvalidData,
format!(
"refusing to read config `{}`: file grew past the {CONFIG_SURFACE_MAX_BYTES}-byte cap after open",
path.display()
),
),
});
}
let mut bytes = Vec::new();
if let Err(source) = file
.take(CONFIG_SURFACE_MAX_BYTES.saturating_add(1))
.read_to_end(&mut bytes)
{
return Err(ConfigSurfaceError::Read {
path: path.to_path_buf(),
source,
});
}
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > CONFIG_SURFACE_MAX_BYTES {
return Err(ConfigSurfaceError::Read {
path: path.to_path_buf(),
source: io::Error::new(
io::ErrorKind::InvalidData,
format!(
"refusing to read config `{}`: file grew past the {CONFIG_SURFACE_MAX_BYTES}-byte cap after the metadata check (TOCTOU)",
path.display()
),
),
});
}
let contents = String::from_utf8(bytes).map_err(|error| ConfigSurfaceError::Read {
path: path.to_path_buf(),
source: io::Error::new(
io::ErrorKind::InvalidData,
format!(
"refusing to read config `{}`: contents are not valid UTF-8: {error}",
path.display()
),
),
})?;
Ok(Some(contents))
}
fn open_config_surface_file_for_read_no_follow(path: &Path) -> io::Result<fs::File> {
let mut options = fs::OpenOptions::new();
options.read(true);
configure_config_surface_open_no_follow(&mut options);
options.open(path)
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn configure_config_surface_open_no_follow(options: &mut fs::OpenOptions) {
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
}
#[cfg(not(all(unix, not(any(target_os = "espidf", target_os = "horizon")))))]
fn configure_config_surface_open_no_follow(_options: &mut fs::OpenOptions) {}
fn ensure_config_write_path_is_regular_or_missing(path: &Path) -> Result<(), io::Error> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_file() => Ok(()),
Ok(_) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"refusing to write config `{}` because it is not a regular file",
path.display()
),
)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
fn ensure_config_temp_path_is_missing(path: &Path) -> Result<(), io::Error> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_file() => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"refusing to write config temp `{}` because it already exists",
path.display()
),
)),
Ok(_) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"refusing to write config temp `{}` because it is not a regular file",
path.display()
),
)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
fn publish_config_temp_file(path: &Path, temp_path: &Path) -> Result<(), ConfigSurfaceError> {
ensure_no_config_symlink_components(path, "publish").map_err(|source| {
ConfigSurfaceError::Write {
path: path.to_path_buf(),
source,
}
})?;
ensure_config_write_path_is_regular_or_missing(path).map_err(|source| {
ConfigSurfaceError::Write {
path: path.to_path_buf(),
source,
}
})?;
ensure_no_config_symlink_components(temp_path, "publish temp").map_err(|source| {
ConfigSurfaceError::Write {
path: temp_path.to_path_buf(),
source,
}
})?;
ensure_config_created_temp_path_is_regular(temp_path).map_err(|source| {
ConfigSurfaceError::Write {
path: temp_path.to_path_buf(),
source,
}
})?;
fs::rename(temp_path, path).map_err(|source| ConfigSurfaceError::Write {
path: path.to_path_buf(),
source,
})?;
if let Some(parent) = path.parent() {
if let Ok(dir) = fs::File::open(parent) {
let _ = dir.sync_data();
}
}
Ok(())
}
fn ensure_config_created_temp_path_is_regular(path: &Path) -> Result<(), io::Error> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_file() => Ok(()),
Ok(_) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"refusing to publish config temp `{}` because it is not a regular file",
path.display()
),
)),
Err(error) => Err(error),
}
}
fn ensure_no_config_symlink_components(
path: &Path,
operation: &'static str,
) -> Result<(), io::Error> {
let mut current = PathBuf::new();
for component in path.components() {
current.push(component.as_os_str());
match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"refusing to {operation} config `{}` through symlinked path component `{}`",
path.display(),
current.display()
),
));
}
Ok(_) => {}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
return Ok(());
}
Err(error) => return Err(error),
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GraphValueKind {
Bool,
UnitFloat,
PositiveFloat,
NonNegativeFloat,
UnsignedInteger,
PositiveInteger,
PercentInteger,
UnsignedIntegerMap,
RerankMode,
SearchSpeed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct GraphKeySpec {
key: &'static str,
path: &'static [&'static str],
kind: GraphValueKind,
}
fn config_key_spec(key: &str) -> Option<GraphKeySpec> {
match key {
SEARCH_DEFAULT_SPEED_KEY => Some(GraphKeySpec {
key: SEARCH_DEFAULT_SPEED_KEY,
path: &["search", "default_speed"],
kind: GraphValueKind::SearchSpeed,
}),
SEARCH_LEXICAL_WEIGHT_KEY => Some(GraphKeySpec {
key: SEARCH_LEXICAL_WEIGHT_KEY,
path: &["search", "lexical_weight"],
kind: GraphValueKind::UnitFloat,
}),
SEARCH_SEMANTIC_WEIGHT_KEY => Some(GraphKeySpec {
key: SEARCH_SEMANTIC_WEIGHT_KEY,
path: &["search", "semantic_weight"],
kind: GraphValueKind::UnitFloat,
}),
SEARCH_GRAPH_WEIGHT_KEY => Some(GraphKeySpec {
key: SEARCH_GRAPH_WEIGHT_KEY,
path: &["search", "graph_weight"],
kind: GraphValueKind::UnitFloat,
}),
SEARCH_RERANK_KEY => Some(GraphKeySpec {
key: SEARCH_RERANK_KEY,
path: &["search", "rerank"],
kind: GraphValueKind::RerankMode,
}),
SEARCH_RERANK_TOP_K_KEY => Some(GraphKeySpec {
key: SEARCH_RERANK_TOP_K_KEY,
path: &["search", "rerank_top_k"],
kind: GraphValueKind::PositiveInteger,
}),
MEMORY_INCLUDE_GLOBAL_KEY => Some(GraphKeySpec {
key: MEMORY_INCLUDE_GLOBAL_KEY,
path: &["memory", "include_global"],
kind: GraphValueKind::Bool,
}),
MEMORY_PARTICIPATE_KEY => Some(GraphKeySpec {
key: MEMORY_PARTICIPATE_KEY,
path: &["memory", "participate"],
kind: GraphValueKind::Bool,
}),
_ => graph_key_spec(key),
}
}
fn graph_key_spec(key: &str) -> Option<GraphKeySpec> {
match key {
GRAPH_PPR_ALPHA_KEY => Some(GraphKeySpec {
key: GRAPH_PPR_ALPHA_KEY,
path: &["graph", "ppr", "alpha"],
kind: GraphValueKind::UnitFloat,
}),
GRAPH_HEALTH_CONTRADICTION_THRESHOLD_KEY => Some(GraphKeySpec {
key: GRAPH_HEALTH_CONTRADICTION_THRESHOLD_KEY,
path: &["graph", "health", "contradiction_threshold"],
kind: GraphValueKind::UnitFloat,
}),
GRAPH_CURATE_ONION_DECAY_MAX_KEY => Some(GraphKeySpec {
key: GRAPH_CURATE_ONION_DECAY_MAX_KEY,
path: &["graph", "curate", "onion_decay_max"],
kind: GraphValueKind::PositiveFloat,
}),
GRAPH_CURATE_ARTICULATION_PROTECTION_MULTIPLIER_KEY => Some(GraphKeySpec {
key: GRAPH_CURATE_ARTICULATION_PROTECTION_MULTIPLIER_KEY,
path: &["graph", "curate", "articulation_protection_multiplier"],
kind: GraphValueKind::UnitFloat,
}),
GRAPH_HITS_PROFILE_BOOST_KEY => Some(GraphKeySpec {
key: GRAPH_HITS_PROFILE_BOOST_KEY,
path: &["graph", "hits", "profile_boost"],
kind: GraphValueKind::NonNegativeFloat,
}),
GRAPH_CAUSAL_MIN_COST_NORMALIZATION_KEY => Some(GraphKeySpec {
key: GRAPH_CAUSAL_MIN_COST_NORMALIZATION_KEY,
path: &["graph", "causal", "min_cost_normalization"],
kind: GraphValueKind::PositiveFloat,
}),
GRAPH_PACK_DNA_MAX_ITEMS_KEY => Some(GraphKeySpec {
key: GRAPH_PACK_DNA_MAX_ITEMS_KEY,
path: &["graph", "pack_dna", "max_items"],
kind: GraphValueKind::UnsignedInteger,
}),
GRAPH_PACK_DNA_MAX_EDGES_KEY => Some(GraphKeySpec {
key: GRAPH_PACK_DNA_MAX_EDGES_KEY,
path: &["graph", "pack_dna", "max_edges"],
kind: GraphValueKind::UnsignedInteger,
}),
GRAPH_GOMORY_HU_SAMPLE_THRESHOLD_KEY => Some(GraphKeySpec {
key: GRAPH_GOMORY_HU_SAMPLE_THRESHOLD_KEY,
path: &["graph", "gomory_hu", "sample_threshold"],
kind: GraphValueKind::UnsignedInteger,
}),
GRAPH_GOMORY_HU_SAMPLE_SIZE_KEY => Some(GraphKeySpec {
key: GRAPH_GOMORY_HU_SAMPLE_SIZE_KEY,
path: &["graph", "gomory_hu", "sample_size"],
kind: GraphValueKind::UnsignedInteger,
}),
GRAPH_MEMORY_SNAPSHOT_CAP_MB_KEY => Some(GraphKeySpec {
key: GRAPH_MEMORY_SNAPSHOT_CAP_MB_KEY,
path: &["graph", "memory", "snapshot_cap_mb"],
kind: GraphValueKind::PositiveInteger,
}),
GRAPH_MEMORY_PER_ALGORITHM_CAP_MB_KEY => Some(GraphKeySpec {
key: GRAPH_MEMORY_PER_ALGORITHM_CAP_MB_KEY,
path: &["graph", "memory", "per_algorithm_cap_mb"],
kind: GraphValueKind::PositiveInteger,
}),
GRAPH_MEMORY_DEGRADED_BELOW_PCT_KEY => Some(GraphKeySpec {
key: GRAPH_MEMORY_DEGRADED_BELOW_PCT_KEY,
path: &["graph", "memory", "degraded_below_pct"],
kind: GraphValueKind::PercentInteger,
}),
GRAPH_MEMORY_GROWTH_MULTIPLIER_BASIS_POINTS_KEY => Some(GraphKeySpec {
key: GRAPH_MEMORY_GROWTH_MULTIPLIER_BASIS_POINTS_KEY,
path: &["graph", "memory", "growth_multiplier_basis_points"],
kind: GraphValueKind::PositiveInteger,
}),
GRAPH_WITNESSES_RETENTION_DAYS_KEY => Some(GraphKeySpec {
key: GRAPH_WITNESSES_RETENTION_DAYS_KEY,
path: &["graph", "witnesses", "retention_days"],
kind: GraphValueKind::UnsignedInteger,
}),
GRAPH_WITNESSES_ALGORITHM_TTL_DAYS_KEY => Some(GraphKeySpec {
key: GRAPH_WITNESSES_ALGORITHM_TTL_DAYS_KEY,
path: &["graph", "witnesses", "algorithm_ttl_days"],
kind: GraphValueKind::UnsignedIntegerMap,
}),
GRAPH_FEATURE_PPR_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_PPR_ENABLED_KEY,
path: &["graph", "feature", "ppr", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_PACK_DNA_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_PACK_DNA_ENABLED_KEY,
path: &["graph", "feature", "pack_dna", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_CAUSAL_EXPLAIN_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_CAUSAL_EXPLAIN_ENABLED_KEY,
path: &["graph", "feature", "causal_explain", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_STRUCTURAL_HEALTH_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_STRUCTURAL_HEALTH_ENABLED_KEY,
path: &["graph", "feature", "structural_health", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_STRUCTURAL_DECAY_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_STRUCTURAL_DECAY_ENABLED_KEY,
path: &["graph", "feature", "structural_decay", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_PROXIMITY_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_PROXIMITY_ENABLED_KEY,
path: &["graph", "feature", "proximity", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_REVISION_DOMINANCE_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_REVISION_DOMINANCE_ENABLED_KEY,
path: &["graph", "feature", "revision_dominance", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_SKYLINE_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_SKYLINE_ENABLED_KEY,
path: &["graph", "feature", "skyline", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_LOAD_BEARING_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_LOAD_BEARING_ENABLED_KEY,
path: &["graph", "feature", "load_bearing", "enabled"],
kind: GraphValueKind::Bool,
}),
GRAPH_FEATURE_HITS_PROFILES_ENABLED_KEY => Some(GraphKeySpec {
key: GRAPH_FEATURE_HITS_PROFILES_ENABLED_KEY,
path: &["graph", "feature", "hits_profiles", "enabled"],
kind: GraphValueKind::Bool,
}),
_ => None,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum TomlScalar {
Bool(bool),
Float(f64),
Integer(i64),
String(&'static str),
}
impl TomlScalar {
fn report_value(self) -> String {
match self {
Self::Bool(value) => value.to_string(),
Self::Float(value) => value.to_string(),
Self::Integer(value) => value.to_string(),
Self::String(value) => value.to_string(),
}
}
}
fn parse_graph_value(spec: GraphKeySpec, raw: &str) -> Result<TomlScalar, ConfigSurfaceError> {
match spec.kind {
GraphValueKind::Bool => match raw {
"true" => Ok(TomlScalar::Bool(true)),
"false" => Ok(TomlScalar::Bool(false)),
_ => Err(invalid_value(spec, raw, "`true` or `false`")),
},
GraphValueKind::UnitFloat => {
let value = parse_finite_float(spec, raw, "a finite number in the range 0.0..=1.0")?;
if (0.0..=1.0).contains(&value) {
Ok(TomlScalar::Float(value))
} else {
Err(invalid_value(
spec,
raw,
"a finite number in the range 0.0..=1.0",
))
}
}
GraphValueKind::PositiveFloat => {
let value = parse_finite_float(spec, raw, "a finite number greater than 0.0")?;
if value > 0.0 {
Ok(TomlScalar::Float(value))
} else {
Err(invalid_value(spec, raw, "a finite number greater than 0.0"))
}
}
GraphValueKind::NonNegativeFloat => {
let value =
parse_finite_float(spec, raw, "a finite number greater than or equal to 0.0")?;
if value >= 0.0 {
Ok(TomlScalar::Float(value))
} else {
Err(invalid_value(
spec,
raw,
"a finite number greater than or equal to 0.0",
))
}
}
GraphValueKind::UnsignedInteger => {
let value = raw
.parse::<u64>()
.map_err(|_| invalid_value(spec, raw, "a non-negative integer"))?;
if value <= i64::MAX as u64 {
Ok(TomlScalar::Integer(value as i64))
} else {
Err(invalid_value(
spec,
raw,
"a non-negative integer <= i64::MAX",
))
}
}
GraphValueKind::PositiveInteger => {
let value = raw
.parse::<u64>()
.map_err(|_| invalid_value(spec, raw, "a positive integer"))?;
if value == 0 {
Err(invalid_value(spec, raw, "a positive integer"))
} else if value <= i64::MAX as u64 {
Ok(TomlScalar::Integer(value as i64))
} else {
Err(invalid_value(spec, raw, "a positive integer <= i64::MAX"))
}
}
GraphValueKind::PercentInteger => {
let value = raw
.parse::<u64>()
.map_err(|_| invalid_value(spec, raw, "an integer in the range 0..=100"))?;
if value <= 100 {
Ok(TomlScalar::Integer(value as i64))
} else {
Err(invalid_value(spec, raw, "an integer in the range 0..=100"))
}
}
GraphValueKind::UnsignedIntegerMap => Err(invalid_value(
spec,
raw,
"a table of per-algorithm non-negative integers in `.ee/config.toml`, such as `[graph.witnesses.algorithm_ttl_days] personalized_pagerank = 90`, or use `ee maintenance graph-witnesses-prune --algorithm-ttl personalized_pagerank=90`",
)),
GraphValueKind::RerankMode => match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(TomlScalar::String("auto")),
"off" => Ok(TomlScalar::String("off")),
_ => Err(invalid_value(spec, raw, "`auto` or `off`")),
},
GraphValueKind::SearchSpeed => match raw.trim().to_ascii_lowercase().as_str() {
"fast" => Ok(TomlScalar::String("fast")),
"balanced" => Ok(TomlScalar::String("balanced")),
"thorough" => Ok(TomlScalar::String("thorough")),
_ => Err(invalid_value(
spec,
raw,
"`fast`, `balanced`, or `thorough`",
)),
},
}
}
fn parse_finite_float(
spec: GraphKeySpec,
raw: &str,
expected: &'static str,
) -> Result<f64, ConfigSurfaceError> {
let value = raw
.parse::<f64>()
.map_err(|_| invalid_value(spec, raw, expected))?;
if value.is_finite() {
Ok(value)
} else {
Err(invalid_value(spec, raw, expected))
}
}
fn invalid_value(spec: GraphKeySpec, raw: &str, expected: &'static str) -> ConfigSurfaceError {
ConfigSurfaceError::InvalidValue {
key: spec.key,
value: raw.to_owned(),
expected,
}
}
fn item_for_path<'a>(document: &'a DocumentMut, path: &[&str]) -> Option<&'a Item> {
let mut item = document.as_table().get(path.first()?)?;
for key in &path[1..] {
item = item.get(*key)?;
}
Some(item)
}
fn item_value_for_report(item: &Item) -> String {
if let Some(value) = item.as_float() {
value.to_string()
} else if let Some(value) = item.as_integer() {
value.to_string()
} else if let Some(value) = item.as_bool() {
value.to_string()
} else if let Some(value) = item.as_str() {
value.to_string()
} else {
item.type_name().to_string()
}
}
fn set_toml_value(document: &mut DocumentMut, path: &[&str], value: TomlScalar) {
if path.is_empty() {
return;
}
let mut current = &mut document[path[0]];
for &segment in &path[1..] {
current = &mut current[segment];
}
*current = match value {
TomlScalar::Bool(value) => toml_edit::value(value),
TomlScalar::Float(value) => toml_edit::value(value),
TomlScalar::Integer(value) => toml_edit::value(value),
TomlScalar::String(value) => toml_edit::value(value),
};
}
#[cfg(test)]
mod tests {
use super::{
ConfigSurfaceOptions, ensure_config_write_path_is_regular_or_missing, get_config,
graph_config_keys, merged_config_with_environment, publish_config_temp_file, set_config,
show_config,
};
use crate::config::{
ConfigValueSource, HANDOFF_STALE_ANY_EXPIRED_IN_PACK_KEY,
HANDOFF_STALE_CONTENT_DRIFT_SCORE_KEY, HANDOFF_STALE_MEMORIES_ADDED_KEY,
HANDOFF_STALE_MEMORIES_REVISED_KEY, POLICY_SECRET_DETECTOR_ALLOW_REGEX_KEY,
SEARCH_DEFAULT_SPEED_KEY,
};
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs;
type TestResult = Result<(), String>;
fn workspace() -> Result<tempfile::TempDir, String> {
tempfile::tempdir().map_err(|error| format!("tempdir: {error}"))
}
fn options(root: &std::path::Path) -> ConfigSurfaceOptions {
ConfigSurfaceOptions {
workspace_root: root.to_path_buf(),
config_path: None,
}
}
#[test]
fn graph_show_filters_to_graph_namespace() -> TestResult {
let temp = workspace()?;
let report = show_config(&options(temp.path()), Some("graph.*"))
.map_err(|error| error.to_string())?;
let keys = report
.entries
.iter()
.map(|entry| entry.key)
.collect::<Vec<_>>();
if keys == graph_config_keys() {
Ok(())
} else {
Err(format!("unexpected graph keys: {keys:?}"))
}
}
#[test]
fn merged_config_loads_user_layer_below_project_layer() -> TestResult {
let temp = workspace()?;
let workspace_root = temp.path().join("workspace");
let user_home = temp.path().join("home");
fs::create_dir_all(workspace_root.join(".ee")).map_err(|error| error.to_string())?;
fs::create_dir_all(user_home.join(".config").join("ee"))
.map_err(|error| error.to_string())?;
fs::write(
user_home.join(".config").join("ee").join("config.toml"),
"[policy.secret_detector]\nallow_regex = ['user-pattern']\n",
)
.map_err(|error| error.to_string())?;
let home_variable = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
let mut environment = BTreeMap::new();
environment.insert(
home_variable.to_owned(),
OsString::from(user_home.as_os_str()),
);
let merged = merged_config_with_environment(&options(&workspace_root), &environment)
.map_err(|error| error.to_string())?;
if merged.values.policy.secret_detector.allow_regex != Some(vec!["user-pattern".to_owned()])
{
return Err(format!("unexpected user-layer config: {merged:?}"));
}
if merged.source(POLICY_SECRET_DETECTOR_ALLOW_REGEX_KEY) != Some(ConfigValueSource::User) {
return Err(format!("unexpected user-layer source: {merged:?}"));
}
fs::write(
workspace_root.join(".ee").join("config.toml"),
"[policy.secret_detector]\nallow_regex = ['project-pattern']\n",
)
.map_err(|error| error.to_string())?;
let merged = merged_config_with_environment(&options(&workspace_root), &environment)
.map_err(|error| error.to_string())?;
if merged.values.policy.secret_detector.allow_regex
!= Some(vec!["project-pattern".to_owned()])
{
return Err(format!("unexpected project-layer config: {merged:?}"));
}
if merged.source(POLICY_SECRET_DETECTOR_ALLOW_REGEX_KEY) != Some(ConfigValueSource::Project)
{
return Err(format!("unexpected project-layer source: {merged:?}"));
}
Ok(())
}
#[test]
fn handoff_show_filters_to_handoff_namespace_with_sources() -> TestResult {
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
fs::write(
config_dir.join("config.toml"),
"\
[handoff.stale_threshold]
memories_added = 7
content_drift_score = 0.25
",
)
.map_err(|error| error.to_string())?;
let report = show_config(&options(temp.path()), Some("handoff.*"))
.map_err(|error| error.to_string())?;
if report.entry_count != 4 {
return Err(format!("unexpected handoff entry count: {report:?}"));
}
let entry = |key| {
report
.entries
.iter()
.find(|entry| entry.key == key)
.ok_or_else(|| format!("missing handoff config key {key}"))
};
let memories_added = entry(HANDOFF_STALE_MEMORIES_ADDED_KEY)?;
if memories_added.value != "7" || memories_added.source != "project" {
return Err(format!(
"unexpected memories_added entry: {memories_added:?}"
));
}
let content_drift = entry(HANDOFF_STALE_CONTENT_DRIFT_SCORE_KEY)?;
if content_drift.value != "0.25" || content_drift.source != "project" {
return Err(format!("unexpected content_drift entry: {content_drift:?}"));
}
let any_expired = entry(HANDOFF_STALE_ANY_EXPIRED_IN_PACK_KEY)?;
if any_expired.value != "true" || any_expired.source != "default" {
return Err(format!("unexpected any_expired entry: {any_expired:?}"));
}
let memories_revised = entry(HANDOFF_STALE_MEMORIES_REVISED_KEY)?;
if memories_revised.value != "0" || memories_revised.source != "default" {
return Err(format!(
"unexpected memories_revised entry: {memories_revised:?}"
));
}
Ok(())
}
#[test]
fn graph_get_accepts_every_advertised_graph_key() -> TestResult {
let temp = workspace()?;
for key in graph_config_keys() {
get_config(&options(temp.path()), key).map_err(|error| {
format!("advertised graph key `{key}` must be gettable: {error}")
})?;
}
Ok(())
}
#[test]
fn graph_get_reads_default_with_source() -> TestResult {
let temp = workspace()?;
let report = get_config(&options(temp.path()), "graph.ppr.alpha")
.map_err(|error| error.to_string())?;
if report.value != "0.3" {
return Err(format!("unexpected graph.ppr.alpha: {}", report.value));
}
if report.source != "default" {
return Err(format!("unexpected source: {}", report.source));
}
Ok(())
}
#[test]
fn graph_witness_get_reads_defaults_with_source() -> TestResult {
let temp = workspace()?;
let retention = get_config(&options(temp.path()), "graph.witnesses.retention_days")
.map_err(|error| error.to_string())?;
let algorithm_ttls =
get_config(&options(temp.path()), "graph.witnesses.algorithm_ttl_days")
.map_err(|error| error.to_string())?;
if retention.value != "30" {
return Err(format!(
"unexpected graph.witnesses.retention_days: {}",
retention.value
));
}
if retention.source != "default" {
return Err(format!("unexpected retention source: {}", retention.source));
}
if algorithm_ttls.value != "0" {
return Err(format!(
"unexpected graph.witnesses.algorithm_ttl_days count: {}",
algorithm_ttls.value
));
}
if algorithm_ttls.source != "default" {
return Err(format!(
"unexpected algorithm TTL source: {}",
algorithm_ttls.source
));
}
Ok(())
}
#[test]
fn graph_witness_get_reads_project_algorithm_ttl_count() -> TestResult {
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
fs::write(
config_dir.join("config.toml"),
"\
[graph.witnesses]
retention_days = 45
[graph.witnesses.algorithm_ttl_days]
personalized_pagerank = 90
cache_results = 120
",
)
.map_err(|error| error.to_string())?;
let retention = get_config(&options(temp.path()), "graph.witnesses.retention_days")
.map_err(|error| error.to_string())?;
let algorithm_ttls =
get_config(&options(temp.path()), "graph.witnesses.algorithm_ttl_days")
.map_err(|error| error.to_string())?;
if retention.value != "45" || retention.source != "project" {
return Err(format!(
"unexpected project retention report: value={} source={}",
retention.value, retention.source
));
}
if algorithm_ttls.value != "2" || algorithm_ttls.source != "project" {
return Err(format!(
"unexpected project algorithm TTL report: value={} source={}",
algorithm_ttls.value, algorithm_ttls.source
));
}
Ok(())
}
#[test]
fn search_default_speed_get_reads_default_with_source() -> TestResult {
let temp = workspace()?;
let report =
get_config(&options(temp.path()), SEARCH_DEFAULT_SPEED_KEY).map_err(|error| {
format!("search.default_speed should be gettable because show reports it: {error}")
})?;
if report.value != "balanced" {
return Err(format!("unexpected search.default_speed: {}", report.value));
}
if report.source != "default" {
return Err(format!("unexpected source: {}", report.source));
}
Ok(())
}
#[test]
fn search_weight_get_reads_default_with_source() -> TestResult {
let temp = workspace()?;
let report = get_config(&options(temp.path()), "search.semantic_weight")
.map_err(|error| error.to_string())?;
if report.value != "0.45" {
return Err(format!(
"unexpected search.semantic_weight: {}",
report.value
));
}
if report.source != "default" {
return Err(format!("unexpected source: {}", report.source));
}
Ok(())
}
#[test]
fn rerank_get_reads_defaults_with_source() -> TestResult {
let temp = workspace()?;
let mode = get_config(&options(temp.path()), "search.rerank")
.map_err(|error| error.to_string())?;
let top_k = get_config(&options(temp.path()), "search.rerank_top_k")
.map_err(|error| error.to_string())?;
if mode.value != "auto" {
return Err(format!("unexpected search.rerank: {}", mode.value));
}
if mode.source != "default" {
return Err(format!("unexpected rerank source: {}", mode.source));
}
if top_k.value != "50" {
return Err(format!("unexpected search.rerank_top_k: {}", top_k.value));
}
if top_k.source != "default" {
return Err(format!("unexpected rerank_top_k source: {}", top_k.source));
}
Ok(())
}
#[test]
fn search_default_speed_set_round_trips_supported_values() -> TestResult {
let temp = workspace()?;
for value in ["fast", "balanced", "thorough"] {
let report = set_config(
&options(temp.path()),
SEARCH_DEFAULT_SPEED_KEY,
value,
false,
)
.map_err(|error| format!("set search.default_speed={value}: {error}"))?;
if !report.applied && report.before.as_deref() != Some(report.value.as_str()) {
return Err(format!(
"search.default_speed={value} did not apply or report idempotence"
));
}
let observed = get_config(&options(temp.path()), SEARCH_DEFAULT_SPEED_KEY)
.map_err(|error| format!("get search.default_speed after {value}: {error}"))?;
if observed.value != value {
return Err(format!(
"expected search.default_speed {value}, got {}",
observed.value
));
}
if observed.source != "project" {
return Err(format!(
"search.default_speed={value}: unexpected source {}",
observed.source
));
}
}
Ok(())
}
#[test]
fn search_weight_set_round_trips_supported_keys() -> TestResult {
let temp = workspace()?;
let samples = [
("search.lexical_weight", "0.95"),
("search.semantic_weight", "0.05"),
("search.graph_weight", "0.0"),
];
for (key, value) in samples {
let report = set_config(&options(temp.path()), key, value, false)
.map_err(|error| format!("set {key}: {error}"))?;
if !report.applied && report.before.as_deref() != Some(report.value.as_str()) {
return Err(format!("{key} did not apply or report idempotence"));
}
let observed = get_config(&options(temp.path()), key)
.map_err(|error| format!("get {key}: {error}"))?;
if observed.value != report.value {
return Err(format!(
"{key}: expected {}, got {}",
report.value, observed.value
));
}
if observed.source != "project" {
return Err(format!("{key}: unexpected source {}", observed.source));
}
}
Ok(())
}
#[test]
fn rerank_set_round_trips_supported_keys() -> TestResult {
let temp = workspace()?;
let samples = [("search.rerank", "off"), ("search.rerank_top_k", "7")];
for (key, value) in samples {
let report = set_config(&options(temp.path()), key, value, false)
.map_err(|error| format!("set {key}: {error}"))?;
if !report.applied && report.before.as_deref() != Some(report.value.as_str()) {
return Err(format!("{key} did not apply or report idempotence"));
}
let observed = get_config(&options(temp.path()), key)
.map_err(|error| format!("get {key}: {error}"))?;
if observed.value != report.value {
return Err(format!(
"{key}: expected {}, got {}",
report.value, observed.value
));
}
if observed.source != "project" {
return Err(format!("{key}: unexpected source {}", observed.source));
}
}
Ok(())
}
#[test]
fn search_weight_set_rejects_invalid_ranges() -> TestResult {
let temp = workspace()?;
let error = match set_config(
&options(temp.path()),
"search.semantic_weight",
"1.5",
false,
) {
Ok(report) => return Err(format!("invalid weight unexpectedly succeeded: {report:?}")),
Err(error) => error.to_string(),
};
if error.contains("0.0..=1.0") {
Ok(())
} else {
Err(format!("unexpected error: {error}"))
}
}
#[test]
fn rerank_set_rejects_invalid_values() -> TestResult {
let temp = workspace()?;
let speed_error = match set_config(
&options(temp.path()),
SEARCH_DEFAULT_SPEED_KEY,
"instant",
false,
) {
Ok(report) => {
return Err(format!(
"invalid search speed unexpectedly succeeded: {report:?}"
));
}
Err(error) => error.to_string(),
};
if !speed_error.contains("`fast`, `balanced`, or `thorough`") {
return Err(format!("unexpected search speed error: {speed_error}"));
}
let mode_error = match set_config(&options(temp.path()), "search.rerank", "always", false) {
Ok(report) => {
return Err(format!(
"invalid rerank mode unexpectedly succeeded: {report:?}"
));
}
Err(error) => error.to_string(),
};
if !mode_error.contains("`auto` or `off`") {
return Err(format!("unexpected rerank mode error: {mode_error}"));
}
let top_k_error = match set_config(&options(temp.path()), "search.rerank_top_k", "0", false)
{
Ok(report) => {
return Err(format!(
"invalid rerank top-k unexpectedly succeeded: {report:?}"
));
}
Err(error) => error.to_string(),
};
if !top_k_error.contains("positive integer") {
return Err(format!("unexpected rerank top-k error: {top_k_error}"));
}
Ok(())
}
#[test]
fn graph_set_round_trips_all_supported_keys() -> TestResult {
let temp = workspace()?;
let samples = [
("graph.ppr.alpha", "0.0"),
("graph.health.contradiction_threshold", "1.0"),
("graph.curate.onion_decay_max", "4.5"),
("graph.curate.articulation_protection_multiplier", "0.75"),
("graph.hits.profile_boost", "0.0"),
("graph.causal.min_cost_normalization", "2.0"),
("graph.pack_dna.max_items", "12"),
("graph.pack_dna.max_edges", "34"),
("graph.gomory_hu.sample_threshold", "600"),
("graph.gomory_hu.sample_size", "150"),
("graph.memory.snapshot_cap_mb", "128"),
("graph.memory.per_algorithm_cap_mb", "64"),
("graph.memory.degraded_below_pct", "75"),
("graph.memory.growth_multiplier_basis_points", "12500"),
("graph.witnesses.retention_days", "45"),
("graph.feature.ppr.enabled", "true"),
("graph.feature.pack_dna.enabled", "true"),
("graph.feature.causal_explain.enabled", "false"),
("graph.feature.structural_health.enabled", "true"),
("graph.feature.structural_decay.enabled", "true"),
("graph.feature.proximity.enabled", "false"),
("graph.feature.revision_dominance.enabled", "false"),
("graph.feature.skyline.enabled", "true"),
("graph.feature.load_bearing.enabled", "false"),
("graph.feature.hits_profiles.enabled", "true"),
];
for (key, value) in samples {
let report = set_config(&options(temp.path()), key, value, false)
.map_err(|error| format!("set {key}: {error}"))?;
if !report.applied && report.before.as_deref() != Some(report.value.as_str()) {
return Err(format!("{key} did not apply or report idempotence"));
}
let observed = get_config(&options(temp.path()), key)
.map_err(|error| format!("get {key}: {error}"))?;
if observed.value != report.value {
return Err(format!(
"{key}: expected {}, got {}",
report.value, observed.value
));
}
if observed.source != "project" {
return Err(format!("{key}: unexpected source {}", observed.source));
}
}
Ok(())
}
#[test]
fn graph_set_rejects_algorithm_ttl_table_key_with_specific_hint() -> TestResult {
let temp = workspace()?;
let error = match set_config(
&options(temp.path()),
"graph.witnesses.algorithm_ttl_days",
"90",
false,
) {
Ok(report) => {
return Err(format!(
"algorithm TTL table set unexpectedly succeeded: {report:?}"
));
}
Err(error) => error.to_string(),
};
if !error.contains("table of per-algorithm non-negative integers") {
return Err(format!("unexpected algorithm TTL table error: {error}"));
}
if !error.contains("personalized_pagerank = 90") {
return Err(format!(
"algorithm TTL error must show TOML example: {error}"
));
}
Ok(())
}
#[test]
fn graph_set_rejects_invalid_ranges() -> TestResult {
let temp = workspace()?;
let error = match set_config(&options(temp.path()), "graph.ppr.alpha", "1.5", false) {
Ok(report) => return Err(format!("invalid alpha unexpectedly succeeded: {report:?}")),
Err(error) => error.to_string(),
};
if !error.contains("0.0..=1.0") {
return Err(format!("unexpected error: {error}"));
}
let invalid_memory_values = [
("graph.memory.snapshot_cap_mb", "0", "positive integer"),
("graph.memory.per_algorithm_cap_mb", "0", "positive integer"),
("graph.memory.degraded_below_pct", "101", "0..=100"),
(
"graph.memory.growth_multiplier_basis_points",
"0",
"positive integer",
),
];
for (key, value, expected) in invalid_memory_values {
let error = match set_config(&options(temp.path()), key, value, false) {
Ok(report) => {
return Err(format!("{key}={value} unexpectedly succeeded: {report:?}"));
}
Err(error) => error.to_string(),
};
if !error.contains(expected) {
return Err(format!(
"{key}={value}: expected error containing `{expected}`, got: {error}"
));
}
}
Ok(())
}
#[test]
fn graph_set_rejects_invalid_feature_flag_bool() -> TestResult {
let temp = workspace()?;
let flag_keys = graph_config_keys()
.iter()
.copied()
.filter(|key| key.starts_with("graph.feature.") && key.ends_with(".enabled"))
.collect::<Vec<_>>();
if flag_keys.len() != 10 {
return Err(format!(
"expected 10 graph feature flags, got {flag_keys:?}"
));
}
for key in flag_keys {
let error = match set_config(&options(temp.path()), key, "yes", false) {
Ok(report) => {
return Err(format!(
"invalid graph feature flag {key} unexpectedly succeeded: {report:?}"
));
}
Err(error) => error.to_string(),
};
if !error.contains("`true` or `false`") {
return Err(format!("{key}: unexpected error: {error}"));
}
}
Ok(())
}
#[cfg(unix)]
#[test]
fn graph_set_rejects_symlinked_metadata_parent() -> TestResult {
use std::os::unix::fs::symlink;
let temp = workspace()?;
let real_metadata = temp.path().join("real-ee");
fs::create_dir_all(&real_metadata).map_err(|error| error.to_string())?;
symlink(&real_metadata, temp.path().join(".ee")).map_err(|error| error.to_string())?;
let error = set_config(&options(temp.path()), "graph.ppr.alpha", "0.5", false)
.expect_err("symlinked .ee parent should reject config set")
.to_string();
if !error.contains("symlinked path component") {
return Err(format!("unexpected symlink error: {error}"));
}
if real_metadata.join("config.toml").exists() {
return Err("config set wrote through symlinked .ee parent".to_string());
}
Ok(())
}
#[cfg(unix)]
#[test]
fn graph_get_rejects_symlinked_config_file() -> TestResult {
use std::os::unix::fs::symlink;
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
let outside_config = temp.path().join("outside-config.toml");
fs::write(
&outside_config,
"[graph.ppr]\nalpha = 0.9\n[graph.feature.ppr]\nenabled = true\n",
)
.map_err(|error| error.to_string())?;
symlink(&outside_config, config_dir.join("config.toml"))
.map_err(|error| error.to_string())?;
let error = get_config(&options(temp.path()), "graph.ppr.alpha")
.expect_err("symlinked config file should reject config get")
.to_string();
if error.contains("symlinked path component") {
Ok(())
} else {
Err(format!("unexpected symlink error: {error}"))
}
}
#[cfg(unix)]
#[test]
fn graph_get_final_read_open_rejects_symlink_leaf() -> TestResult {
use std::os::unix::fs::symlink;
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
let outside_config = temp.path().join("outside-config.toml");
fs::write(
&outside_config,
"[graph.ppr]\nalpha = 0.9\n[graph.feature.ppr]\nenabled = true\n",
)
.map_err(|error| error.to_string())?;
let linked_config = config_dir.join("config.toml");
symlink(&outside_config, &linked_config).map_err(|error| error.to_string())?;
let result = super::open_config_surface_file_for_read_no_follow(&linked_config);
assert!(
result.is_err(),
"final config-surface read open must reject a symlink leaf"
);
if fs::read_to_string(&outside_config).map_err(|error| error.to_string())?
!= "[graph.ppr]\nalpha = 0.9\n[graph.feature.ppr]\nenabled = true\n"
{
return Err("symlink target should remain untouched".to_string());
}
Ok(())
}
#[test]
fn graph_get_rejects_non_regular_config_path() -> TestResult {
let temp = workspace()?;
let config_path = temp.path().join(".ee").join("config.toml");
fs::create_dir_all(&config_path).map_err(|error| error.to_string())?;
let error = get_config(&options(temp.path()), "graph.ppr.alpha")
.expect_err("directory config path should reject config get")
.to_string();
if error.contains("not a regular file") {
Ok(())
} else {
Err(format!("unexpected non-regular config error: {error}"))
}
}
#[test]
fn graph_get_rejects_oversize_config_file() -> TestResult {
let temp = workspace()?;
let ee_dir = temp.path().join(".ee");
fs::create_dir(&ee_dir).map_err(|error| error.to_string())?;
let config_path = ee_dir.join("config.toml");
let cap = usize::try_from(super::CONFIG_SURFACE_MAX_BYTES)
.map_err(|error| format!("cap fits in usize: {error}"))?;
let mut payload = String::with_capacity(cap + 1);
while payload.len() <= cap {
payload.push('#');
}
fs::write(&config_path, &payload).map_err(|error| error.to_string())?;
let error = get_config(&options(temp.path()), "graph.ppr.alpha")
.expect_err("oversize config should reject config get before unbounded allocation")
.to_string();
if !error.contains("exceeding the") {
return Err(format!(
"rejection message must cite the ceiling; got: {error}"
));
}
if !error.contains(&super::CONFIG_SURFACE_MAX_BYTES.to_string()) {
return Err(format!(
"rejection message must name the cap constant; got: {error}"
));
}
Ok(())
}
#[test]
fn config_write_preflight_rejects_non_regular_final_path() -> TestResult {
let temp = workspace()?;
let config_path = temp.path().join(".ee").join("config.toml");
fs::create_dir_all(&config_path).map_err(|error| error.to_string())?;
let error = ensure_config_write_path_is_regular_or_missing(&config_path)
.expect_err("write preflight should reject a directory config path");
let message = error.to_string();
if !message.contains("not a regular file") {
return Err(format!("unexpected non-regular config error: {message}"));
}
if !config_path.is_dir() {
return Err(
"write preflight should leave non-regular config path untouched".to_owned(),
);
}
Ok(())
}
#[cfg(unix)]
#[test]
fn graph_set_rejects_symlinked_temp_config_before_write() -> TestResult {
use std::os::unix::fs::symlink;
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
let outside_config = temp.path().join("outside-config.toml");
fs::write(&outside_config, "outside sentinel").map_err(|error| error.to_string())?;
symlink(&outside_config, config_dir.join("config.tmp"))
.map_err(|error| error.to_string())?;
let error = set_config(&options(temp.path()), "graph.ppr.alpha", "0.5", false)
.expect_err("symlinked config temp path should reject config set")
.to_string();
if !error.contains("symlinked path component") {
return Err(format!("unexpected symlink temp error: {error}"));
}
let outside_after =
fs::read_to_string(&outside_config).map_err(|error| error.to_string())?;
if outside_after != "outside sentinel" {
return Err("config set must not write through a symlinked temp path".to_owned());
}
if config_dir.join("config.toml").exists() {
return Err("config final path should not be created after temp rejection".to_owned());
}
Ok(())
}
#[test]
fn graph_set_rejects_non_regular_temp_config_before_write() -> TestResult {
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
let temp_path = config_dir.join("config.tmp");
fs::create_dir_all(&temp_path).map_err(|error| error.to_string())?;
let error = set_config(&options(temp.path()), "graph.ppr.alpha", "0.5", false)
.expect_err("directory config temp path should reject config set")
.to_string();
if !error.contains("not a regular file") {
return Err(format!("unexpected non-regular temp error: {error}"));
}
if !temp_path.is_dir() {
return Err("config set should leave non-regular temp path untouched".to_owned());
}
if config_dir.join("config.toml").exists() {
return Err("config final path should not be created after temp rejection".to_owned());
}
Ok(())
}
#[test]
fn graph_set_rejects_existing_regular_temp_config_without_truncating() -> TestResult {
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
let temp_path = config_dir.join("config.tmp");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
fs::write(&temp_path, "stale config temp").map_err(|error| error.to_string())?;
let error = set_config(&options(temp.path()), "graph.ppr.alpha", "0.5", false)
.expect_err("existing regular temp path should reject config set")
.to_string();
if !error.contains("already exists") {
return Err(format!("unexpected existing temp error: {error}"));
}
let temp_after = fs::read_to_string(&temp_path).map_err(|error| error.to_string())?;
if temp_after != "stale config temp" {
return Err("config set must not truncate an existing regular temp file".to_owned());
}
if config_dir.join("config.toml").exists() {
return Err("config final path should not be created after temp rejection".to_owned());
}
Ok(())
}
#[cfg(unix)]
#[test]
fn graph_set_publish_rechecks_final_symlink_before_rename() -> TestResult {
use std::os::unix::fs::symlink;
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
let config_path = config_dir.join("config.toml");
let temp_path = config_dir.join("config.tmp");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
fs::write(&temp_path, "[graph.ppr]\nalpha = 0.5\n").map_err(|error| error.to_string())?;
let outside_config = temp.path().join("outside-config.toml");
fs::write(&outside_config, "outside sentinel").map_err(|error| error.to_string())?;
symlink(&outside_config, &config_path).map_err(|error| error.to_string())?;
let error = publish_config_temp_file(&config_path, &temp_path)
.expect_err("final config symlink should reject publish")
.to_string();
if !error.contains("symlinked path component") {
return Err(format!("unexpected final symlink publish error: {error}"));
}
let outside_after =
fs::read_to_string(&outside_config).map_err(|error| error.to_string())?;
if outside_after != "outside sentinel" {
return Err("config publish must not overwrite symlink target".to_owned());
}
if !fs::symlink_metadata(&temp_path)
.map_err(|error| error.to_string())?
.file_type()
.is_file()
{
return Err("config temp file should remain after final publish rejection".to_owned());
}
Ok(())
}
#[cfg(unix)]
#[test]
fn graph_set_publish_rechecks_temp_symlink_before_rename() -> TestResult {
use std::os::unix::fs::symlink;
let temp = workspace()?;
let config_dir = temp.path().join(".ee");
let config_path = config_dir.join("config.toml");
let temp_path = config_dir.join("config.tmp");
let temp_backup = config_dir.join("config.tmp.backup");
fs::create_dir_all(&config_dir).map_err(|error| error.to_string())?;
fs::write(&temp_path, "[graph.ppr]\nalpha = 0.5\n").map_err(|error| error.to_string())?;
fs::rename(&temp_path, &temp_backup).map_err(|error| error.to_string())?;
let outside_config = temp.path().join("outside-temp-config.toml");
fs::write(&outside_config, "outside sentinel").map_err(|error| error.to_string())?;
symlink(&outside_config, &temp_path).map_err(|error| error.to_string())?;
let error = publish_config_temp_file(&config_path, &temp_path)
.expect_err("temp config symlink should reject publish")
.to_string();
if !error.contains("symlinked path component") {
return Err(format!("unexpected temp symlink publish error: {error}"));
}
let outside_after =
fs::read_to_string(&outside_config).map_err(|error| error.to_string())?;
if outside_after != "outside sentinel" {
return Err("config publish must not follow temp symlink target".to_owned());
}
if !fs::symlink_metadata(&temp_path)
.map_err(|error| error.to_string())?
.file_type()
.is_symlink()
{
return Err("config temp symlink should remain after publish rejection".to_owned());
}
if config_path.exists() {
return Err("config final path should not be published from temp symlink".to_owned());
}
Ok(())
}
}