Files
Kigi-CLI/crates/codegen/kigi-config/src/campaigns.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

290 lines
9.6 KiB
Rust

//! `[[campaigns]]` overlays. Priority (first id wins): requirements > remote >
//! user > managed > system_managed. Applied after layer merge.
use serde::{Deserialize, Serialize};
use crate::config_override::{
ConfigOverrideEntry, PATCH_STRIP_KEYS, PatchPath, apply_patches, patch_touches_any,
take_patch_array,
};
pub const CAMPAIGNS_KEY: &str = "campaigns";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CampaignMeta {
#[serde(default, alias = "campaign_id")]
pub id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CampaignEntry {
pub id: String,
pub patch: toml::Table,
}
/// Disk campaigns grouped by source layer. Merged with the remote layer (by
/// priority, first id wins) in [`crate::loader::ConfigLayers::resolve_campaigns`].
#[derive(Debug, Clone, Default)]
pub struct CampaignOverrides {
pub requirements: Vec<CampaignEntry>,
pub user: Vec<CampaignEntry>,
pub managed: Vec<CampaignEntry>,
pub system_managed: Vec<CampaignEntry>,
}
pub fn take_campaigns(config: &mut toml::Value) -> Vec<ConfigOverrideEntry<CampaignMeta>> {
match take_patch_array::<CampaignMeta>(config, CAMPAIGNS_KEY) {
Ok(entries) => entries,
Err(e) => {
tracing::warn!(error = %e, "campaigns: failed to deserialize; ignoring entries");
Vec::new()
}
}
}
pub fn build_campaign_entries(
taken: Vec<ConfigOverrideEntry<CampaignMeta>>,
layer: &'static str,
) -> Vec<CampaignEntry> {
let mut out = Vec::with_capacity(taken.len());
for entry in taken {
let Some(id) = entry
.meta
.id
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
else {
tracing::warn!(layer, "campaigns: entry missing id; skipped");
continue;
};
if entry.patch.is_empty() {
continue;
}
out.push(CampaignEntry {
id,
patch: entry.patch,
});
}
out
}
/// `sources` in priority order; first `id` wins.
pub fn merge_campaign_entries(sources: &[&[CampaignEntry]]) -> Vec<CampaignEntry> {
let mut seen = std::collections::HashSet::<String>::new();
let mut out = Vec::new();
for source in sources {
for entry in *source {
if !seen.insert(entry.id.clone()) {
continue;
}
out.push(entry.clone());
}
}
out
}
/// Drop dismissed ids from a priority-merged list, preserving order.
pub fn filter_active_campaigns(
merged: Vec<CampaignEntry>,
dismissed_ids: &std::collections::HashSet<String>,
) -> Vec<CampaignEntry> {
merged
.into_iter()
.filter(|e| !dismissed_ids.contains(&e.id))
.collect()
}
/// Ids of `active` campaigns whose patch touches any of `paths` — used to dismiss
/// campaigns when the user persists a value at one of those paths.
pub fn ids_touching_paths(active: &[CampaignEntry], paths: &[PatchPath]) -> Vec<String> {
active
.iter()
.filter(|e| patch_touches_any(&e.patch, paths))
.map(|e| e.id.clone())
.collect()
}
/// `active` is highest-priority-first; patches apply lowest-first (`.rev()`) so the
/// highest-priority source wins a leaf conflict.
pub fn apply_active_campaign_patches(effective: &mut toml::Value, active: &[CampaignEntry]) {
apply_patches(
effective,
active.iter().rev().map(|e| e.patch.clone()),
PATCH_STRIP_KEYS,
);
}
pub fn take_campaign_entries(config: &mut toml::Value, layer: &'static str) -> Vec<CampaignEntry> {
build_campaign_entries(take_campaigns(config), layer)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(s: &str) -> toml::Value {
toml::from_str(s).unwrap()
}
fn models_default_patch(default: &str) -> toml::Table {
let mut models = toml::map::Map::new();
models.insert("default".into(), toml::Value::String(default.into()));
let mut t = toml::map::Map::new();
t.insert("models".into(), toml::Value::Table(models));
t
}
#[test]
fn campaign_overlays_any_field_over_user_config() {
let mut layer = parse(
r#"
[[campaigns]]
id = "c1"
[campaigns.models]
default = "new-model"
[campaigns.features]
web_fetch = true
"#,
);
let entries = take_campaign_entries(&mut layer, "managed");
assert!(layer.get(CAMPAIGNS_KEY).is_none());
assert_eq!(entries.len(), 1);
let mut effective =
parse("[models]\ndefault = \"old-model\"\n[features]\nweb_fetch = false\n");
apply_active_campaign_patches(&mut effective, &entries);
assert_eq!(effective["models"]["default"].as_str(), Some("new-model"));
assert_eq!(effective["features"]["web_fetch"].as_bool(), Some(true));
}
#[test]
fn merge_first_source_wins_duplicate_id() {
let req = [CampaignEntry {
id: "same".into(),
patch: models_default_patch("from-req"),
}];
let remote = [CampaignEntry {
id: "same".into(),
patch: models_default_patch("from-remote"),
}];
let merged = merge_campaign_entries(&[&req, &remote]);
assert_eq!(merged.len(), 1);
assert_eq!(
merged[0].patch["models"]["default"].as_str(),
Some("from-req")
);
}
#[test]
fn apply_highest_priority_wins_on_leaf_conflict() {
// Two *distinct* ids both set models.default, so dedup by id does not
// apply and the leaf conflict is settled by apply order alone.
let req = [CampaignEntry {
id: "req".into(),
patch: models_default_patch("from-req"),
}];
let managed = [CampaignEntry {
id: "managed".into(),
patch: models_default_patch("from-managed"),
}];
let merged = merge_campaign_entries(&[&req, &managed]);
assert_eq!(merged.len(), 2);
let mut effective = parse("[models]\ndefault = \"user-old\"\n");
apply_active_campaign_patches(&mut effective, &merged);
assert_eq!(effective["models"]["default"].as_str(), Some("from-req"));
}
#[test]
fn build_campaign_entries_skips_missing_id() {
let taken = vec![
ConfigOverrideEntry {
meta: CampaignMeta { id: None },
patch: models_default_patch("dropped-none"),
},
ConfigOverrideEntry {
meta: CampaignMeta {
id: Some(" ".into()),
},
patch: models_default_patch("dropped-blank"),
},
ConfigOverrideEntry {
meta: CampaignMeta {
id: Some("valid".into()),
},
patch: models_default_patch("kept"),
},
];
let out = build_campaign_entries(taken, "managed");
assert_eq!(out.len(), 1);
assert_eq!(out[0].id, "valid");
assert_eq!(out[0].patch["models"]["default"].as_str(), Some("kept"));
}
#[test]
fn campaign_id_alias_is_accepted_in_toml_and_does_not_leak_into_patch() {
for src in [
"[[campaigns]]\ncampaign_id = \"c1\"\n[campaigns.models]\ndefault = \"m\"\n",
"[[campaigns]]\nid = \"c1\"\n[campaigns.models]\ndefault = \"m\"\n",
] {
let mut layer = parse(src);
let entries = take_campaign_entries(&mut layer, "user");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].id, "c1");
// A leaked id key would deep-merge a junk top-level `id` into every
// effective config.
assert!(
entries[0].patch.get("id").is_none()
&& entries[0].patch.get("campaign_id").is_none(),
"id keys must not leak into the patch: {src}"
);
}
}
#[test]
fn requirements_win_over_campaign() {
use crate::loader::ConfigLayers;
// A campaign (even from a lower layer) can't override a field the admin
// set in requirements: `apply_campaign_overrides` re-merges requirements on top.
let mut layers = ConfigLayers {
user: parse("[models]\ndefault = \"user-old\"\n"),
user_requirements: Some(parse("[models]\ndefault = \"pinned\"\n")),
..Default::default()
};
layers.campaigns.user = vec![CampaignEntry {
id: "c1".into(),
patch: models_default_patch("campaign"),
}];
let effective =
layers.effective_config_with_campaigns(&[], &std::collections::HashSet::new());
assert_eq!(
effective["models"]["default"].as_str(),
Some("pinned"),
"requirements must beat a campaign for the same field"
);
}
#[test]
fn effective_config_honors_dismiss() {
use crate::loader::ConfigLayers;
let mut layers = ConfigLayers {
user: parse("[models]\ndefault = \"user-old\"\n"),
..Default::default()
};
layers.campaigns.managed = vec![CampaignEntry {
id: "c1".into(),
patch: models_default_patch("new"),
}];
let none = std::collections::HashSet::new();
let active = layers.effective_config_with_campaigns(&[], &none);
assert_eq!(active["models"]["default"].as_str(), Some("new"));
let dismissed: std::collections::HashSet<_> = ["c1".into()].into_iter().collect();
let off = layers.effective_config_with_campaigns(&[], &dismissed);
assert_eq!(off["models"]["default"].as_str(), Some("user-old"));
}
}