Skip to main content

remem/rules/
management.rs

1use anyhow::{bail, ensure, Context, Result};
2use rusqlite::{params, Connection};
3use std::path::Path;
4
5use super::{
6    artifact_path_for_project, load_artifact_fail_open, ArtifactLoad, CompiledRule, RuleAction,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ProjectRules {
11    pub project: String,
12    pub compiled_at_epoch: i64,
13    pub rules: Vec<CompiledRule>,
14}
15
16pub fn list_project_rules(data_dir: &Path, project: &str) -> Result<ProjectRules> {
17    let path = artifact_path_for_project(data_dir, project);
18    let artifact = match load_artifact_fail_open(&path) {
19        ArtifactLoad::Loaded(artifact) => artifact,
20        ArtifactLoad::FailOpen { message, .. } => bail!(message),
21    };
22    Ok(ProjectRules {
23        project: project.to_string(),
24        compiled_at_epoch: artifact.compiled_at_epoch,
25        rules: artifact.rules,
26    })
27}
28
29pub fn set_rule_disabled(
30    conn: &Connection,
31    data_dir: &Path,
32    project: &str,
33    rule_id: &str,
34    disabled: bool,
35) -> Result<()> {
36    update_rule_override(
37        conn,
38        data_dir,
39        project,
40        rule_id,
41        RuleOverrideUpdate::Disabled(disabled),
42    )
43}
44
45pub fn set_rule_action(
46    conn: &Connection,
47    data_dir: &Path,
48    project: &str,
49    rule_id: &str,
50    action: RuleAction,
51    pre_execution_supported: bool,
52) -> Result<()> {
53    if action == RuleAction::Block && !pre_execution_supported {
54        bail!(
55            "block action requires a supported pre-execution host; Codex command enforcement is unsupported, so use 'warn' or select Claude Code explicitly"
56        );
57    }
58    update_rule_override(
59        conn,
60        data_dir,
61        project,
62        rule_id,
63        RuleOverrideUpdate::Action(action),
64    )
65}
66
67enum RuleOverrideUpdate {
68    Disabled(bool),
69    Action(RuleAction),
70}
71
72fn update_rule_override(
73    conn: &Connection,
74    data_dir: &Path,
75    project: &str,
76    rule_id: &str,
77    update: RuleOverrideUpdate,
78) -> Result<()> {
79    let project_rules = list_project_rules(data_dir, project)?;
80    let artifact_rule = project_rules
81        .rules
82        .iter()
83        .find(|rule| rule.rule_id == rule_id)
84        .with_context(|| format!("compiled rule '{rule_id}' not found for project '{project}'"))?;
85    let config = crate::runtime_config::rule_compilation_config()
86        .context("read rule compilation config before rule override")?;
87    ensure!(
88        config.enabled,
89        "rule compilation is disabled; enable rule_compilation.enabled before changing compiled rule overrides"
90    );
91
92    let tx = conn.unchecked_transaction()?;
93    let current_rules = crate::rules::compile_project_rules(&tx, project, config)
94        .context("validate current rule eligibility before override")?;
95    let current_rule = current_rules
96        .rules
97        .iter()
98        .find(|rule| rule.rule_id == rule_id)
99        .with_context(|| {
100            format!(
101                "compiled rule '{rule_id}' is stale and no longer eligible for project '{project}'"
102            )
103        })?;
104    ensure!(
105        artifact_rule.source_memory_id == current_rule.source_memory_id
106            && artifact_rule.predicate == current_rule.predicate,
107        "compiled rule '{rule_id}' is stale; wait for the pending worker rebuild before changing it"
108    );
109
110    execute_override_upsert(&tx, project, current_rule, update)?;
111    crate::memory::preference::compilation::enqueue_project_required(&tx, project)?;
112    tx.commit()?;
113    Ok(())
114}
115
116fn execute_override_upsert(
117    conn: &Connection,
118    project: &str,
119    rule: &CompiledRule,
120    update: RuleOverrideUpdate,
121) -> Result<()> {
122    let now = chrono::Utc::now().timestamp();
123    match update {
124        RuleOverrideUpdate::Disabled(disabled) => conn.execute(
125            "INSERT INTO preference_rule_overrides
126                 (project, rule_id, source_memory_id, disabled, action_override,
127                  reason, updated_by, updated_at_epoch)
128                 VALUES (?1, ?2, ?3, ?4, ?5, 'remem rules CLI', 'user', ?6)
129                 ON CONFLICT(project, rule_id) DO UPDATE SET
130                   source_memory_id = excluded.source_memory_id,
131                   disabled = excluded.disabled,
132                   reason = excluded.reason,
133                   updated_by = excluded.updated_by,
134                   updated_at_epoch = excluded.updated_at_epoch",
135            params![
136                project,
137                rule.rule_id,
138                rule.source_memory_id,
139                i64::from(disabled),
140                rule.override_state
141                    .action_override
142                    .map(rule_action_db_value),
143                now
144            ],
145        ),
146        RuleOverrideUpdate::Action(action) => conn.execute(
147            "INSERT INTO preference_rule_overrides
148                 (project, rule_id, source_memory_id, disabled, action_override,
149                  reason, updated_by, updated_at_epoch)
150                 VALUES (?1, ?2, ?3, ?4, ?5, 'remem rules CLI', 'user', ?6)
151                 ON CONFLICT(project, rule_id) DO UPDATE SET
152                   source_memory_id = excluded.source_memory_id,
153                   action_override = excluded.action_override,
154                   reason = excluded.reason,
155                   updated_by = excluded.updated_by,
156                   updated_at_epoch = excluded.updated_at_epoch",
157            params![
158                project,
159                rule.rule_id,
160                rule.source_memory_id,
161                i64::from(rule.override_state.disabled),
162                rule_action_db_value(action),
163                now
164            ],
165        ),
166    }
167    .with_context(|| format!("persist override for compiled rule '{}'", rule.rule_id))?;
168    Ok(())
169}
170
171fn rule_action_db_value(action: RuleAction) -> &'static str {
172    match action {
173        RuleAction::Warn => "warn",
174        RuleAction::Block => "block",
175    }
176}
177
178#[cfg(test)]
179mod tests;