diff --git a/crates/ente-brain/schema/rule.k b/crates/ente-brain/schema/rule.k index f7fb468..432cde5 100644 --- a/crates/ente-brain/schema/rule.k +++ b/crates/ente-brain/schema/rule.k @@ -1,10 +1,18 @@ # ============================================================================ -# rule.k — Triplet [Sujeto + Evento + Acción(Objeto)]. La gramática del -# Cerebro del fractal. Cada regla es una sinapsis: cuando ocurre `when`, -# el motor ejecuta `then` para todos los Entes que cumplen `scope`. +# rule.k — REFERENCE ONLY. NOT LOADED. # -# El motor en Rust las indexa por discriminante de EventKind para lookup +# La gramática autoritativa de Rule vive en Rust: +# crates/ente-brain/src/rules.rs +# El loader (crates/ente-brain/src/loader.rs) sólo acepta JSON / JSONL. +# +# Conservado como notas de diseño humano-legibles del shape Rule: +# Triplet [Sujeto + Evento + Acción(Objeto)]. Cada regla es una sinapsis: +# cuando ocurre `when`, el motor ejecuta `then` para los Entes que cumplen +# `scope`. El motor las indexa por discriminante de EventKind para lookup # en O(1). Las reglas son inmutables tras carga (Arc). +# +# Si cambias el shape en Rust, sincroniza este archivo a mano (o +# reemplázalo por JSON Schema generado vía `schemars`). # ============================================================================ schema Rule: diff --git a/crates/ente-brain/src/loader.rs b/crates/ente-brain/src/loader.rs index 93a9a50..296ebc1 100644 --- a/crates/ente-brain/src/loader.rs +++ b/crates/ente-brain/src/loader.rs @@ -97,3 +97,76 @@ pub fn extract_rules_from_json(raw: &str) -> anyhow::Result> { } Ok(rules) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::introspect::append_rule_jsonl; + use crate::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope}; + use ulid::Ulid; + + fn sample_rule() -> Rule { + Rule { + id: Ulid::new(), + priority: 5, + when: EventPattern::Single { kind: EventKind::EnteSpawned }, + then: vec![Action::Log { + level: LogLevel::Info, + message: "test".into(), + }], + scope: Scope::default(), + } + } + + #[test] + fn rules_from_array() { + let r = sample_rule(); + let raw = format!("[{}]", serde_json::to_string(&r).unwrap()); + let parsed = extract_rules_from_json(&raw).expect("array parse"); + assert_eq!(parsed.len(), 1); + } + + #[test] + fn rules_from_object_with_array() { + let r = sample_rule(); + let raw = format!(r#"{{"rules":[{}]}}"#, serde_json::to_string(&r).unwrap()); + let parsed = extract_rules_from_json(&raw).expect("object parse"); + assert_eq!(parsed.len(), 1); + } + + #[test] + fn rules_from_jsonl_with_comments_and_blanks() { + let r1 = sample_rule(); + let r2 = sample_rule(); + let raw = format!( + "# header comment\n\n{}\n# inline comment\n{}\n\n", + serde_json::to_string(&r1).unwrap(), + serde_json::to_string(&r2).unwrap() + ); + let parsed = extract_rules_from_json(&raw).expect("jsonl parse"); + assert_eq!(parsed.len(), 2); + } + + #[test] + fn append_rule_jsonl_roundtrip() { + let dir = tempdir_unique(); + let path = dir.join("rules.jsonl"); + let r1 = sample_rule(); + let r2 = sample_rule(); + append_rule_jsonl(&path, &r1).expect("append 1"); + append_rule_jsonl(&path, &r2).expect("append 2"); + let raw = std::fs::read_to_string(&path).expect("read back"); + let parsed = extract_rules_from_json(&raw).expect("roundtrip parse"); + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0].id, r1.id); + assert_eq!(parsed[1].id, r2.id); + let _ = std::fs::remove_dir_all(&dir); + } + + fn tempdir_unique() -> std::path::PathBuf { + let base = std::env::temp_dir(); + let p = base.join(format!("ente-brain-loader-{}", Ulid::new())); + std::fs::create_dir_all(&p).unwrap(); + p + } +} diff --git a/crates/ente-card/schema/card.k b/crates/ente-card/schema/card.k index bc92b35..1c1232b 100644 --- a/crates/ente-card/schema/card.k +++ b/crates/ente-card/schema/card.k @@ -1,14 +1,14 @@ # ============================================================================ -# card.k — Genética del Ente. Esquema KCL para EntityCard. +# card.k — REFERENCE ONLY. NOT LOADED. # -# Esta es la gramática autoritativa: cualquier Card que se cargue al fractal -# debe pasar la validación de este esquema. El boot de PID 1 acepta JSON que -# cumple este shape (KCL exporta JSON tras validar `check:`). +# La validación canónica de EntityCard vive en Rust: +# crates/ente-card/src/lib.rs :: EntityCard::validate() +# El loader (crates/ente-brain/src/loader.rs) sólo acepta JSON. # -# Para validar manualmente: -# kcl run examples/my-card.k --schema schema/card.k -# -# Cada `check:` es invariante de fractal. Romperlo = Card inválida = no boot. +# Este archivo se conserva como notas de diseño legibles para humanos sobre +# las invariantes que `validate()` debe garantizar. Si modificas el shape +# en Rust, sincroniza este archivo a mano (o reemplázalo por JSON Schema +# generado vía `schemars`). # ============================================================================ # ---------- Identidad ----------