use std::collections::BTreeMap;
use std::path::Path;
use serde::Deserialize;
use super::types::EnvSourceKind;
pub fn default_table() -> BTreeMap<EnvSourceKind, u8> {
use EnvSourceKind::*;
let mut t = BTreeMap::new();
t.insert(SealedSecret, 100);
t.insert(ExternalSecret, 95);
t.insert(K8sSecretStringData, 92);
t.insert(K8sSecret, 90);
t.insert(K8sDeploymentEnv, 85);
t.insert(K8sDeploymentEnvFrom, 82);
t.insert(K8sConfigMap, 80);
t.insert(SopsFile, 78);
t.insert(HelmValues, 65);
t.insert(DockerCompose, 50);
t.insert(DockerComposeEnvFile, 45);
t.insert(Dockerfile, 40);
t.insert(NpmScript, 35);
t.insert(EnvSourceKind::DotEnv, 30);
t.insert(GitHubActionsSecret, 20);
t.insert(GitHubActionsEnv, 15);
t.insert(TauriConf, 12);
t.insert(EnvRc, 8);
t
}
pub fn is_template_path(path: &str) -> bool {
if crate::analyzer::classify::artifact_class(path, None)
== crate::analyzer::classify::ArtifactClass::Template
{
return true;
}
let name = Path::new(path)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(path)
.to_ascii_lowercase();
name.contains("example") || name.contains("template") || name.contains("sample")
}
pub fn refine_dotenv_rank(base: u8, path: &str) -> u8 {
let name = Path::new(path)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(path);
if name.contains("example") || name.contains("template") || name.contains("sample") {
return 5;
}
if name.contains("production") || name.contains(".prod") {
return base.saturating_add(8);
}
if name.contains("staging") || name.contains(".stage") {
return base.saturating_add(4);
}
if name.contains(".local") {
return base.saturating_sub(5);
}
if name.contains(".test") || name.contains(".dev") {
return base.saturating_sub(8);
}
base
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct EnvTruthConfig {
#[serde(default)]
pub precedence: BTreeMap<String, u8>,
#[serde(default)]
pub stale_threshold_days: Option<u32>,
}
pub fn load_config_override(snapshot_root: &Path) -> Option<EnvTruthConfig> {
let config_path = snapshot_root.join(".loctree").join("config.toml");
let raw = std::fs::read_to_string(&config_path).ok()?;
let value: toml::Value = toml::from_str(&raw).ok()?;
let env_truth = value.get("env_truth")?;
let cfg: EnvTruthConfig = env_truth.clone().try_into().ok()?;
Some(cfg)
}
pub fn apply_override(table: &mut BTreeMap<EnvSourceKind, u8>, cfg: &EnvTruthConfig, quiet: bool) {
for (raw_key, rank) in &cfg.precedence {
match key_to_kind(raw_key) {
Some(kind) => {
table.insert(kind, *rank);
}
None if !quiet => {
eprintln!("[loct][env-truth] config: unknown precedence key '{raw_key}' (ignored)");
}
None => {}
}
}
}
fn key_to_kind(raw: &str) -> Option<EnvSourceKind> {
use EnvSourceKind::*;
Some(match raw {
"dot_env" | "dotenv" => DotEnv,
"env_rc" | "envrc" => EnvRc,
"dockerfile" => Dockerfile,
"docker_compose" => DockerCompose,
"docker_compose_env_file" => DockerComposeEnvFile,
"k8s_deployment_env" => K8sDeploymentEnv,
"k8s_deployment_env_from" => K8sDeploymentEnvFrom,
"k8s_config_map" | "configmap" => K8sConfigMap,
"k8s_secret" => K8sSecret,
"k8s_secret_string_data" => K8sSecretStringData,
"sealed_secret" => SealedSecret,
"external_secret" => ExternalSecret,
"sops_file" | "sops" => SopsFile,
"helm_values" => HelmValues,
"github_actions_env" | "gha_env" => GitHubActionsEnv,
"github_actions_secret" | "gha_secret" => GitHubActionsSecret,
"npm_script" => NpmScript,
"tauri_conf" => TauriConf,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_table_populates_every_kind() {
let table = default_table();
assert!(table.contains_key(&EnvSourceKind::SealedSecret));
assert!(table.contains_key(&EnvSourceKind::DotEnv));
assert!(table.contains_key(&EnvSourceKind::GitHubActionsEnv));
let max = table.values().max().copied().unwrap();
assert_eq!(table[&EnvSourceKind::SealedSecret], max);
}
#[test]
fn template_paths_detected() {
assert!(is_template_path(".env.example"));
assert!(is_template_path("config/.env.sample"));
assert!(is_template_path("deploy/.env.template"));
assert!(is_template_path("env.example.local"));
assert!(!is_template_path(".env"));
assert!(!is_template_path(".env.production"));
assert!(!is_template_path(".env.local"));
}
#[test]
fn dotenv_refinement_demotes_example() {
let base = 30;
assert!(refine_dotenv_rank(base, ".env.example") < base);
assert!(refine_dotenv_rank(base, ".env.production") > base);
assert!(refine_dotenv_rank(base, ".env.local") < base);
}
#[test]
fn config_override_replaces_known_keys() {
let cfg = EnvTruthConfig {
precedence: BTreeMap::from([("dot_env".into(), 99), ("unknown_key_xyz".into(), 50)]),
stale_threshold_days: None,
};
let mut table = default_table();
apply_override(&mut table, &cfg, true);
assert_eq!(table[&EnvSourceKind::DotEnv], 99);
}
}