Delta semántico · 7
crates/ente-brain/src/loader.rs
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<Rule>).
+#
+# 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<Vec<Rule>> {
}
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 ----------