Audit→CAS, reload rules, time-decay y forma canónica del hash chain

- AuditLog::flush_to_cas() persiste entries pendientes con bytes canónicos
  (sha=[0;32]) para que CAS-sha == entry.sha. AuditHeadPointer en disco
  tras cada flush — verificación posterior sin escanear el log entero.
- IntrospectRequest::FlushAudit / ReloadRules. brainctl flush-audit / reload.
- Auto-flush task cada 10s cuando --audit-head está configurado.
- ReloadRules { path? } vacía engine + carga (.k vía kcl CLI o .json).
- Observer con time-decay opcional: count * 0.5^(age/half_life).
  conditional_prob y pmi consumen valores decayed transparentemente.
- --brain-half-life flag CLI.
- KCL Rust SDK descartado: kcl-* en crates.io son del proyecto KittyCAD,
  no KusionStack. Subprocess al CLI sigue siendo la vía canónica.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-03 23:16:41 +00:00
parent d6b8f18b43
commit a4fa42c781
6 changed files with 282 additions and 40 deletions
+49
View File
@@ -97,6 +97,12 @@ pub enum IntrospectRequest {
RemoveRule { id: Ulid },
/// Lista las últimas N entradas del audit log. limit=0 = todas.
ListAudit { limit: usize },
/// Persiste todas las entries pendientes al CAS y actualiza el head
/// pointer si el log lo tiene configurado.
FlushAudit,
/// Recarga reglas desde el archivo configurado por --rules-out (o el
/// path provisto). Vacía el engine antes de cargar.
ReloadRules { path: Option<String> },
}
#[derive(Debug, Serialize, Deserialize)]
@@ -114,6 +120,10 @@ pub enum IntrospectResponse {
Removed(bool),
/// Entradas del audit log (más recientes al final).
AuditEntries(Vec<crate::audit::AuditEntry>),
/// Resultado de FlushAudit: cuántas entries se escribieron y SHA del head.
Flushed { written: usize, head_sha: Option<[u8; 32]>, total_flushed: u64 },
/// Resultado de ReloadRules: número total de reglas tras el reload.
Reloaded { count: usize },
Error(String),
}
@@ -296,6 +306,45 @@ impl IntrospectServer {
let audit = self.state.audit.read().await;
IntrospectResponse::AuditEntries(audit.recent(limit).cloned().collect())
}
IntrospectRequest::FlushAudit => {
let mut audit = self.state.audit.write().await;
match audit.flush_to_cas() {
Ok(written) => IntrospectResponse::Flushed {
written,
head_sha: audit.last_flushed_sha(),
total_flushed: audit.flushed_count(),
},
Err(e) => IntrospectResponse::Error(format!("flush_to_cas: {e}")),
}
}
IntrospectRequest::ReloadRules { path } => {
// Path explícito gana sobre el rules_out configurado.
let resolved = path.map(std::path::PathBuf::from)
.or_else(|| self.state.rules_out.as_ref().map(|p| p.as_path().to_path_buf()));
let path = match resolved {
Some(p) => p,
None => return IntrospectResponse::Error(
"ReloadRules sin path y sin rules_out configurado".into()
),
};
let rules = match crate::kcl_loader::load_rules_file(&path) {
Ok(r) => r,
Err(e) => return IntrospectResponse::Error(format!("load: {e}")),
};
// Vaciamos el engine antes de re-cargar — semántica clean-slate.
let mut engine = self.state.engine.write().await;
*engine = crate::engine::RuleEngine::empty();
let count = rules.len();
for r in rules { engine.insert(r); }
drop(engine);
self.state.audit.write().await.append(
crate::audit::AuditAction::LoadRulesFile {
path: path.to_string_lossy().into_owned(),
count,
}
);
IntrospectResponse::Reloaded { count }
}
}
}
}