refactor(nakui-core): KCL → Nickel — kcl_wrapper reemplazado por evaluación in-process

Cierra el plan original. El motor de validación de entities deja
de shellear el binario externo `kcl` y pasa a evaluar Nickel
contracts in-process via la dep nickel-lang (la misma que usa el
brazo de cards). Los 3 schemas de sales/inventory/treasury migran
de .k a .ncl.

nakui-core:
- Nueva dep nickel-lang = "2.0.0".
- Borrado kcl_wrapper.rs.
- Nuevo nickel_validator.rs con vet(schema_path, state, entity)
  que evalúa `let bundle = (import "<schema>") in
  (std.deserialize 'Json m%%"<json>"%%) | bundle.<entity>`.
- executor.rs: KclError → NickelError, KclPre/Post/PostCreate →
  SchemaPre/Post/PostCreate, kcl_check → validate_entity.
  build_schema_bundle ahora emite `(import "X") & (import "Y") & ...`
  en lugar de concatenar bytes (cada .ncl es expresión completa).
- manifest.rs: default schema "schema.ncl", extract_schema_names
  reescrito para sintaxis Nickel record (CapitalCase keys con
  2-space indent).

Schemas migrados:
- sales/schema.ncl: Venta con std.contract.Sequence [record,
  from_predicate] para combinar shape + invariante cross-field
  (total == cantidad * precio_unitario). El patrón directo
  `record | from_predicate` rebota con "missing definition" porque
  el predicate evalúa antes de que el value populate el record;
  documentado en cada schema.
- inventory/schema.ncl, treasury/schema.ncl: idem.
- 3 schema.k viejos borrados; sales/nsmc.json paths actualizados.

Tests: refs Kcl* renombradas; paths .k → .ncl; tests inline que
escribían schema.k cambian a schema.ncl con sintaxis Nickel.
84 tests verdes en nakui-core.

Doc-only borrados:
- crates/core/ente-card/schema/card.k (REFERENCE ONLY).
- crates/core/ente-brain/schema/rule.k (REFERENCE ONLY).

Beneficios: sin dep externa al binario `kcl` (build CI limpio),
errores Nickel en línea con caret pointing al field, mismo motor
que cards (una dep para todo el repo), sin tempfile JSON
intermedio.

Cierra el plan original yahweh + KCL + card.k. Pendientes salen
de nuevo trabajo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-10 02:59:48 +00:00
parent b3a99f38dc
commit b05de24c24
23 changed files with 690 additions and 573 deletions
-167
View File
@@ -1,167 +0,0 @@
# ============================================================================
# rule.k — REFERENCE ONLY. NOT LOADED.
#
# 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:
"""Una sinapsis del fractal. Determinista, sin estado entre disparos."""
id: str # Ulid 26 chars
priority: int = 5 # 0..255, mayor = se ejecuta primero
when: EventPattern
then: [Action]
scope: Scope = Scope {} # qué Entes son sujetos válidos
check:
len(id) == 26, "id debe ser Ulid"
priority >= 0 and priority <= 255, "priority fuera de rango"
len(then) > 0, "regla sin acciones"
# ---------- Subject: alcance del sujeto ----------
schema Scope:
"""Match del sujeto. None en todos los campos = match cualquier Ente."""
subject_id?: str # Ulid exacto
subject_label?: str # label exacto
subject_has_cap?: Capability # Ente que declara esta capacidad
check:
subject_id is None or len(subject_id) == 26, "subject_id no es Ulid"
# ---------- Event: qué dispara la regla ----------
# EventPattern es tagged union recursivo.
#
# Atómicos:
# Single — match un evento por kind
# Sequence — N eventos consecutivos dentro de within_ms
#
# Compuestos (recursivos):
# Either — OR sobre sub-patterns
# All — AND sobre sub-patterns (mismo event/history)
schema EventPattern:
type: "Single" | "Sequence" | "Either" | "All"
kind?: EventKind # Single
kinds?: [EventKind] # Sequence
within_ms?: int = 0
patterns?: [EventPattern] # Either / All (recursivo)
check:
type != "Single" or kind is not None, "Single requiere kind"
type != "Sequence" or (kinds is not None and len(kinds) > 0), \
"Sequence requiere kinds no vacío"
type != "Either" or (patterns is not None and len(patterns) > 0), \
"Either requiere patterns no vacío"
type != "All" or (patterns is not None and len(patterns) > 0), \
"All requiere patterns no vacío"
within_ms is None or within_ms >= 0, "within_ms negativo"
# EventKind con tag interno + payload opcional según tag.
schema EventKind:
tag: "EnteSpawned" | "EnteDied" | "BusAnnounce" | "BusInvoke" | "BusInvokeOf" | "DeviceAdded" | "DeviceRemoved" | "Custom"
cap?: Capability # para BusInvokeOf
custom?: str # para Custom
check:
tag != "BusInvokeOf" or cap is not None, "BusInvokeOf requiere cap"
tag != "Custom" or custom is not None, "Custom requiere custom string"
# ---------- Action: qué hacer ----------
schema Action:
"""Una acción ejecutable por el motor. Tagged union con kind."""
kind: "Log" | "Notify" | "Spawn" | "Invoke" | "Inhibit"
# Log
level?: "trace" | "debug" | "info" | "warn" | "error"
message?: str
# Notify
target_id?: str # Ulid
# Spawn
card_blob?: str # base64-encoded EntityCard JSON
# Invoke
target_cap?: Capability
blob_b64?: str
# Inhibit
reason?: str
check:
kind != "Log" or message is not None, "Log requiere message"
kind != "Notify" or (target_id is not None and message is not None), \
"Notify requiere target_id + message"
kind != "Spawn" or card_blob is not None, "Spawn requiere card_blob"
kind != "Invoke" or target_cap is not None, "Invoke requiere target_cap"
kind != "Inhibit" or reason is not None, "Inhibit requiere reason"
# ---------- Capability: re-export desde card.k para evitar inclusión circular ----------
# En uso real: `import ..ente_card.schema.card` y referencia Capability.
# Aquí declaramos una versión alineada para auto-contención del esquema.
schema Capability:
kind: "FilesystemRoot" | "KernelNetlink" | "Endpoint" | "LegacyLogind" | "Device" | "Spawn" | "Journal"
netlink_family?: "Uevent" | "Route" | "Generic" | "Audit"
endpoint_interface?: str
endpoint_version?: int
device_class?: "Block" | "Tty" | "Input" | "Drm" | "Net" | "Hidraw"
# ============================================================================
# Ejemplo de regla cristalizada (auto-generada por el observador)
# ============================================================================
example_rule = Rule {
id = "01KQQ100000000000000000000"
priority = 5
when = EventPattern {
type = "Single"
kind = EventKind {tag = "EnteSpawned"}
}
scope = Scope {
subject_label = "demo-echo"
}
then = [
Action {
kind = "Log"
level = "info"
message = "demo-echo encarnado, observando para crystallization"
}
]
}
# Ejemplo de regla compuesta: cuando un Ente se anuncia y luego es invocado
# en menos de 500ms, log estructurado para auditoría.
example_sequence = Rule {
id = "01KQQ200000000000000000000"
priority = 7
when = EventPattern {
type = "Sequence"
kinds = [
EventKind {tag = "BusAnnounce"}
EventKind {tag = "BusInvoke"}
]
within_ms = 500
}
scope = Scope {}
then = [
Action {
kind = "Log"
level = "info"
message = "patrón Announce→Invoke detectado <500ms"
}
]
}
-178
View File
@@ -1,178 +0,0 @@
# ============================================================================
# card.k — REFERENCE ONLY. NOT LOADED.
#
# 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.
#
# 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 ----------
schema EntityCard:
"""Tarjeta de Identidad. Inmutable: cambios = nueva Card con nuevo id."""
schema_version: int = 1
id: str # Ulid (26 chars, Crockford base32)
lineage?: str # parent Ulid; None = Ente raíz
label: str # legible, no es identificador
provides: [Capability] = [] # contrato hacia el grafo
requires: [Capability] = [] # contrato del grafo hacia el Ente
soma: SomaSpec # cuerpo: aislamiento + recursos
payload: Payload # cómo encarnar (Wasm/Native/Virtual)
supervision: Supervision # política tras muerte
genesis?: [EntityCard] = [] # hijos a instanciar al encarnar
check:
schema_version == 1, "schema version no soportada"
len(label) > 0, "label vacío"
len(id) == 26, "id debe ser Ulid (26 caracteres)"
# Auto-dependencia: una capacidad no puede estar en requires y provides
all c in requires { c not in provides }, "self-dependency: ${c}"
# ---------- Capacidades (typed enum) ----------
# KCL no tiene sum types nativos; usamos tagged union: `kind` + campos opcionales
# que sólo aplican según el kind. Las invariantes en `check:` aseguran consistencia.
schema Capability:
"""Capacidad tipada del fractal. NUNCA usar strings libres."""
kind: "FilesystemRoot" | "KernelNetlink" | "Endpoint" | "LegacyLogind" | "Device" | "Spawn" | "Journal"
netlink_family?: "Uevent" | "Route" | "Generic" | "Audit"
endpoint_interface?: str # 32-char hex (UUID 16 bytes)
endpoint_version?: int
device_class?: "Block" | "Tty" | "Input" | "Drm" | "Net" | "Hidraw"
check:
kind != "KernelNetlink" or netlink_family is not None, \
"KernelNetlink requiere netlink_family"
kind != "Endpoint" or (endpoint_interface is not None and endpoint_version is not None), \
"Endpoint requiere interface + version"
kind != "Endpoint" or len(endpoint_interface) == 32, \
"endpoint_interface debe ser hex de 32 chars"
kind != "Device" or device_class is not None, \
"Device requiere device_class"
# ---------- Soma: cuerpo + restricciones de recursos ----------
schema SomaSpec:
"""Aislamiento + recursos. Validados por KCL antes de tocar el kernel."""
namespaces: NamespaceSet = NamespaceSet {}
rlimits: ResourceLimits = ResourceLimits {}
cgroup: CgroupSpec = CgroupSpec {}
cpu_affinity?: [int] # CPU pinning
check:
cpu_affinity is None or all c in cpu_affinity { c >= 0 and c < 1024 }, \
"cpu_affinity fuera de rango [0, 1024)"
schema NamespaceSet:
mount: bool = False
pid: bool = False
net: bool = False
uts: bool = False
ipc: bool = False
user: bool = False
cgroup: bool = False
schema ResourceLimits:
"""Restricciones nativas validadas en KCL — el kernel sólo ve valores sanos."""
mem_bytes?: int # RLIMIT_AS
nproc?: int # RLIMIT_NPROC
nofile?: int # RLIMIT_NOFILE
energy_budget_mw?: int # presupuesto energético (futuro)
check:
mem_bytes is None or mem_bytes > 0, "mem_bytes debe ser positivo"
mem_bytes is None or mem_bytes <= 1099511627776, "mem_bytes > 1 TiB sospechoso"
nproc is None or (nproc > 0 and nproc <= 65535), "nproc fuera de rango"
nofile is None or (nofile > 0 and nofile <= 1048576), "nofile fuera de rango"
energy_budget_mw is None or energy_budget_mw > 0, "energy_budget_mw debe ser positivo"
schema CgroupSpec:
"""Cgroup v2: path + weights. cpu_weight 1..10000 según kernel docs."""
path: str = ""
cpu_weight?: int
io_weight?: int
check:
cpu_weight is None or (cpu_weight >= 1 and cpu_weight <= 10000), \
"cpu_weight 1..10000"
io_weight is None or (io_weight >= 1 and io_weight <= 10000), \
"io_weight 1..10000"
# ---------- Payload: tagged union de cómo encarnar ----------
schema Payload:
"""Una variante por Card. Set exactly one of: Wasm, Native, Virtual, Legacy."""
kind: "Wasm" | "Native" | "Virtual" | "Legacy"
# Wasm
module_sha256?: str # hex 64 chars
entry?: str
# Native / Legacy
exec?: str
argv?: [str] = []
envp?: [{str: str}] = []
# Legacy
fakes?: ["SystemdLogind" | "SystemdHostnamed" | "SystemdNotify"] = []
check:
kind != "Wasm" or (module_sha256 is not None and entry is not None), \
"Wasm requiere module_sha256 + entry"
kind != "Wasm" or len(module_sha256) == 64, "module_sha256 debe ser hex de 64 chars"
kind != "Native" or exec is not None, "Native requiere exec"
kind != "Legacy" or exec is not None, "Legacy requiere exec"
# ---------- Supervision ----------
schema Supervision:
kind: "Restart" | "OneShot" | "Delegate"
initial_ms?: int # ms — backoff inicial para Restart
max_ms?: int # ms — backoff máximo
check:
kind != "Restart" or (initial_ms is not None and max_ms is not None), \
"Restart requiere initial_ms + max_ms"
initial_ms is None or initial_ms >= 0, "initial_ms negativo"
max_ms is None or max_ms >= initial_ms or max_ms is None, \
"max_ms < initial_ms es contradictorio"
# ============================================================================
# Herencia: EnteWeb hereda de EnteBase con campos pre-rellenados.
# ============================================================================
schema EnteBase(EntityCard):
"""Base para Entes managed: declara Spawn provider y Journal por defecto."""
schema_version = 1
supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
soma = SomaSpec {
rlimits = ResourceLimits {nofile = 4096}
cgroup = CgroupSpec {path = "ente.slice/managed", cpu_weight = 100}
}
schema EnteWeb(EnteBase):
"""Hereda EnteBase, declara endpoint + cap LegacyLogind como ejemplo."""
provides = [
Capability {kind = "Journal"}
Capability {
kind = "Endpoint"
endpoint_interface = "deadbeefcafe1234deadbeefcafe1234"
endpoint_version = 1
}
]
soma = SomaSpec {
namespaces = NamespaceSet {net = True, mount = True, pid = True}
rlimits = ResourceLimits {nofile = 16384, mem_bytes = 536870912} # 512 MiB
cgroup = CgroupSpec {path = "ente.slice/web", cpu_weight = 200, io_weight = 100}
}