ES | EN

EL CONTRATO YAML COMO FUENTE DE VERDAD: EL CÓDIGO SE REGENERA, EL CONTRATO NO

TAGS: ARQUITECTURA / METODOLOGÍA / LLMs / YAML READ_TIME: 13 MIN
El contrato YAML como fuente de verdad: el código se regenera, el contrato no

En el desarrollo tradicional, la documentación siempre va detrás del código. Se escribe después, se actualiza tarde, y eventualmente miente — porque el código evolucionó y la documentación no. En AAL la relación se invierte: el contrato YAML es la fuente de verdad, el código es una consecuencia del contrato, regenerable en cualquier momento por cualquier LLM que lo reciba. Si el código y el contrato se contradicen, el código está mal.

PROJECT_STATUS: STABLE

Stack: YAML · Python · cualquier LLM
Proyectos de referencia: TeliOs · metodología AAL
Objetivo: Hacer el código desechable y el contrato permanente — el riesgo real es perder el YAML, no el código

01. EL PROBLEMA QUE RESUELVE

En el desarrollo tradicional, la única fuente de verdad real termina siendo el código mismo — que es difícil de leer para cualquiera que no lo escribió, y completamente opaco para un LLM que llega sin contexto. El resultado es que cada sesión de trabajo con un LLM empieza desde cero, explicando lo que ya existe.

El contrato YAML elimina ese problema. Define con precisión qué hace cada componente, qué expone, de qué depende, y bajo qué condiciones funciona correctamente. El LLM recibe el contrato y genera el código — no al revés.

// Explicación no técnica

Imagina que construyes muebles. Tienes dos opciones: construir la silla y después intentar dibujar el plano de lo que construiste, o dibujar el plano primero y después construir la silla siguiendo el plano. La primera opción produce planos que siempre están desactualizados. La segunda produce planos que siempre son correctos porque son la fuente de la silla. En AAL, el contrato YAML es el plano. La silla es el código.

02. ANATOMÍA DE UN CONTRATO YAML COMPLETO

Un contrato completo tiene cinco secciones: metadata para identidad y estado, interface para lo que expone, dependencias para lo que necesita, restricciones para los límites no negociables, y tests_aceptacion para la verificación. El contrato de store.js es el ejemplo canónico de TeliOs.

# TELIO-U001.contract.yaml
# Contrato de store.js — motor de estado de TeliOs

metadata:
  id: TELIO-U001
  version: 1.0.0
  nombre: 'TeliOs Store — Motor de Estado Global'
  tipo: utility
  estado: pendiente
  descripcion: >
    Estado único centralizado del ecosistema TeliOs.
    Persiste en localStorage. Notifica cambios via events.js.
    Es la única fuente de verdad del sistema.
    Máximo 100 líneas. Cero dependencias externas.
  lenguaje: JavaScript
  archivo: src/store.js

interface:
  exports:
    getState:
      tipo: function
      descripcion: 'Retorna el estado completo o una clave anidada'
      parametros:
        key: { tipo: string, requerido: false,
               descripcion: 'Clave con notación de punto: kayros.inicio' }
      retorna:
        tipo: any
        descripcion: 'El valor en la clave, o el estado completo si no se especifica'

    setState:
      tipo: function
      descripcion: 'Actualiza estado, persiste en localStorage, notifica cambios'
      parametros:
        key: { tipo: string, requerido: true }
        value: { tipo: any, requerido: true }
      retorna:
        tipo: boolean
        descripcion: 'true si guardó correctamente, false si localStorage falló'

    resetModule:
      tipo: function
      descripcion: 'Reinicia un módulo a sus valores por defecto'
      parametros:
        module: { tipo: string, requerido: true }
      retorna:
        tipo: boolean

dependencias:
  internas:
    - TELIO-U003 # events.js — notifica cambios con emit('state:changed')
  externas: {} # CERO dependencias externas — principio absoluto

restricciones:
  calidad:
    max_lineas: 100
    cobertura_tests: '> 80%'
    sin_dependencias: true
  performance:
    max_tiempo_escritura: '< 10ms'
    max_tamanio_estado: '< 5MB'
  comportamiento:
    debe_notificar: true
    debe_persistir: true
    fallback_memoria: true

tests_aceptacion:
  - id: T001
    nombre: 'setState persiste en localStorage'
    cuando: "setState('arke.tipoSer', 'fuego')"
    entonces:
      - "localStorage contiene clave TELIOS_STATE"
      - "getState('arke.tipoSer') === 'fuego'"
      - "Events recibe emit('state:changed', {key: 'arke.tipoSer', value: 'fuego'})"

  - id: T002
    nombre: 'getState con clave anidada'
    cuando: "setState('kayros.diasTotal', 1096)"
    entonces:
      - "getState('kayros.diasTotal') === 1096"
      - "getState('kayros') es un objeto con diasTotal: 1096"

  - id: T003
    nombre: 'fallback a memoria cuando localStorage falla'
    cuando: 'localStorage.setItem lanza QuotaExceededError'
    entonces:
      - "setState retorna false"
      - "getState sigue funcionando desde memoria"
      - "La app no rompe"

notas_implementacion: >
  Usar IIFE para encapsular.
  No usar clases — módulo funcional puro.
  El estado DEFAULT_STATE debe definirse completo al inicio.
  Las claves anidadas usan notación de punto: 'kayros.inicio'.
Sección Contenido Para qué sirve
metadata ID, versión, tipo, estado, descripción Identidad única del componente. El LLM sabe qué es y para qué existe.
interface Funciones exportadas, parámetros, retornos El contrato público — lo que otros módulos pueden llamar.
dependencias Internas (IDs de células), externas (librerías) Mapa del grafo de dependencias. Detecta ciclos antes de implementar.
restricciones Límites de líneas, performance, comportamiento Los límites no negociables que el LLM no puede ignorar.
tests_aceptacion Escenarios cuando/entonces Criterio de éxito objetivo. Si pasan los tests, la célula está correcta.

03. USAR EL CONTRATO PARA GENERAR EL CÓDIGO

El prompt de implementación en FocOs no describe el problema — entrega el contrato directamente. La instrucción clave es la última: si el contrato y la implementación se contradicen, cambia la implementación. Si el contrato tiene un error, dilo antes de implementar.

# Prompt que FocOs envía al LLM con el contrato:

PROMPT_IMPLEMENTACION = """
Implementa la célula {id} del ecosistema TeliOs.

CONTRATO:
{contenido_yaml}

REGLAS ABSOLUTAS:
1. Máximo {max_lineas} líneas
2. Cero dependencias externas
3. Implementar EXACTAMENTE lo definido en interface.exports
4. Incluir los tests de aceptación como comentarios // TEST: al final
5. JSDoc en cada función exportada

El contrato es la verdad.
Si el contrato y tu implementación se contradicen — cambia la implementación.
Si el contrato tiene un error — dímelo antes de implementar.
"""

04. REGENERAR UNA CÉLULA DESDE EL CONTRATO

Este es el caso de uso que justifica toda la metodología. El código se perdió o se corrompió — pero el contrato YAML existe en el repo. Eso es suficiente para recuperarlo completamente en 15 minutos. Sin el contrato, la misma operación toma horas reconstruyendo desde memoria o desde código roto.

# Situación: el código de store.js se corrompió o se perdió
# El contrato YAML existe — eso es suficiente para regenerar

# Flujo de recuperación:
# 1. Abrir TELIO-U001.contract.yaml
# 2. Pegar el contenido en el prompt de implementación
# 3. El LLM genera store.js desde cero — conforme al contrato
# 4. Verificar los tests de aceptación T001, T002, T003
# 5. Si pasan — célula regenerada. Si no — el LLM itera.

# Tiempo total con contrato: 10-15 minutos
# Tiempo total sin contrato: horas o días
[+] DONDE VIVE EL CONTRATO

Un archivo YAML de 80 líneas en texto plano commiteado en Git es prácticamente imposible de perder. El riesgo en AAL se desplaza por completo: ya no es perder código — es perder el contrato. Y el contrato es mucho más fácil de proteger que el código.

-- CONCLUSION

El contrato YAML como fuente de verdad resuelve el problema más profundo del desarrollo con LLMs: la fragilidad de la base de conocimiento. Si el contrato existe, el código puede perderse, corromperse o quedar obsoleto sin consecuencias permanentes. El LLM puede regenerarlo en 15 minutos. Esto cambia la naturaleza del riesgo en el desarrollo — el riesgo ya no es perder código, es perder el contrato. Y eso es mucho más difícil.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

SQLITE EN APPS DE ESCRITORIO: LA BASE DE DATOS QUE NO NECESITA SERVIDOR

TAGS: BASES DE DATOS / PYTHON / DESKTOP APPS READ_TIME: 12 MIN
SQLite en apps de escritorio: la base de datos que no necesita servidor

Toda app de escritorio necesita que sus datos datos sean persistentes. El instinto del desarrollador web es levantar PostgreSQL o MySQL. Para una app de escritorio personal eso es matar una mosca con un cañón: requiere un servidor corriendo, configuración de usuarios, manejo de conexiones, y complejidad que no aporta nada en un contexto de un solo usuario. SQLite es la alternativa correcta — un solo archivo en disco, cero configuración, cero servidor, cero mantenimiento.

PROJECT_STATUS: STABLE

Stack: Python · SQLite · JSON
Proyecto de referencia: FocOs — persistencia de datos
Objetivo: Persistencia robusta sin servidor — un archivo en disco, años de desarrollo sin tocar el schema

01. EL PROBLEMA QUE RESUELVE

PostgreSQL y MySQL están diseñados para múltiples usuarios concurrentes, alta disponibilidad y cargas de trabajo distribuidas. En una app de escritorio personal, ninguna de esas características aplica — y sin embargo arrastras toda la infraestructura. SQLite resuelve exactamente el problema que tienes: persistencia confiable para un solo usuario, sin proceso externo, sin configuración, sin que el usuario final sepa que existe una base de datos.

// Explicación no técnica

Imagina que necesitas guardar una lista de compras. Tienes dos opciones: contratar a un chef profesional con cocina industrial para que la guarde, o simplemente escribirla en un cuaderno. PostgreSQL es el chef. SQLite es el cuaderno. Para guardar tu lista de compras, el cuaderno es la respuesta correcta — y lo puedes llevar en el bolsillo.

02. LA ESTRUCTURA DE FOCOS EN SQLITE

FocOs usa cuatro tablas principales. Los dos PRAGMAs del inicio son obligatorios: WAL mejora el rendimiento de escritura significativamente, y foreign_keys=ON activa la integridad referencial que SQLite tiene desactivada por defecto.

# main.py — inicialización de la DB

import sqlite3
from pathlib import Path

DB_PATH = Path('data/focos.db')

def init_db():
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    conn.execute("PRAGMA journal_mode=WAL") # Escrituras más rápidas
    conn.execute("PRAGMA foreign_keys=ON") # Integridad referencial

    conn.executescript("""
        CREATE TABLE IF NOT EXISTS projects (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL UNIQUE,
            status TEXT DEFAULT 'active',
            meta TEXT DEFAULT '{}'
        );

        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            project_id INTEGER REFERENCES projects(id),
            title TEXT NOT NULL,
            status TEXT DEFAULT 'pending',
            priority INTEGER DEFAULT 1,
            created_at TEXT DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS workspaces (
            ws_id INTEGER PRIMARY KEY,
            project_id INTEGER REFERENCES projects(id),
            layout TEXT DEFAULT 'default',
            state TEXT DEFAULT '{}'
        );

        CREATE TABLE IF NOT EXISTS sessions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            project_id INTEGER REFERENCES projects(id),
            date TEXT DEFAULT (date('now')),
            duration INTEGER DEFAULT 0,
            notes TEXT DEFAULT ''
        );

        INSERT OR IGNORE INTO workspaces(ws_id) VALUES (1),(2),(3);
    """)
    conn.commit()
    conn.close()

03. CRUD COMPLETO — SIN ORM, SIN MAGIA

El patrón es consistente en todas las operaciones: abrir conexión, ejecutar, commit si escribe, cerrar en el finally. Sin ORM, sin abstracción innecesaria — el SQL es visible, debuggeable y predecible. El soft delete en lugar de DELETE real preserva el historial y evita errores irreversibles.

# CREATE
def create_project(self, name: str, status: str = 'active') -> dict:
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.execute(
            "INSERT INTO projects(name, status) VALUES(?,?)",
            (name, status)
        )
        conn.commit()
        return { 'ok': True, 'id': cur.lastrowid }
    except sqlite3.IntegrityError:
        return { 'ok': False, 'error': f"Proyecto '{name}' ya existe" }
    finally:
        conn.close()

# READ
def get_projects(self) -> list:
    conn = sqlite3.connect(DB_PATH)
    rows = conn.execute(
        "SELECT id, name, status, meta FROM projects ORDER BY id DESC"
    ).fetchall()
    conn.close()
    return [
        { 'id': r[0], 'name': r[1], 'status': r[2],
          'meta': json.loads(r[3] or '{}') }
        for r in rows
    ]

# UPDATE
def update_project_status(self, project_id: int, status: str) -> dict:
    conn = sqlite3.connect(DB_PATH)
    conn.execute(
        "UPDATE projects SET status=? WHERE id=?",
        (status, project_id)
    )
    conn.commit()
    conn.close()
    return { 'ok': True }

# DELETE (soft delete — cambiar status, no borrar)
def archive_project(self, project_id: int) -> dict:
    return self.update_project_status(project_id, 'archived')

04. JSON DENTRO DE SQLITE — EL CAMPO META

SQLite no tiene tipo JSON nativo — se almacena como TEXT y se serializa/deserializa manualmente. La ventaja es que el campo meta puede crecer indefinidamente sin alterar el schema. La regla es clara: datos que necesitan búsqueda van en columnas dedicadas, datos flexibles van en meta.

import json

# Guardar metadata compleja en un solo campo
meta = {
    'tipo': 'software',
    'stack': ['Python', 'JavaScript'],
    'filosofia': 'El foco es la función principal',
    'porcentaje': 85,
}

conn.execute(
    "UPDATE projects SET meta=? WHERE id=?",
    (json.dumps(meta, ensure_ascii=False), project_id)
)

# Leer y deserializar
row = conn.execute(
    "SELECT meta FROM projects WHERE id=?", (project_id,)
).fetchone()
meta = json.loads(row[0]) if row else {}
print(meta['porcentaje']) # 85
Tipo de dato Donde va Razon
ID, status, fechas, foreign keys Columna dedicada Se necesita en WHERE, ORDER BY o JOIN.
Stack, filosofía, porcentaje, config Campo meta (JSON) Solo se lee, no se filtra. Puede crecer sin migración.
Datos que evolucionan rápido Campo meta (JSON) Agregar campos sin ALTER TABLE ni migración.

05. BACKUP AUTOMÁTICO — UNA LÍNEA

SQLite es un archivo. Hacer backup es copiar ese archivo. Sin dumps, sin exports, sin herramientas externas. La estrategia en FocOs es llamar a backup_db() al inicio de cada sesión de trabajo — si algo se corrompe, el backup del día está disponible de forma inmediata.

import shutil
from datetime import datetime

def backup_db(self) -> dict:
    fecha = datetime.now().strftime('%Y-%m-%d')
    backup = DB_PATH.parent / f'focos_backup_{fecha}.db'
    shutil.copy2(DB_PATH, backup)
    return { 'ok': True, 'path': str(backup) }

# Llamar al inicio de cada sesión:
# backup_db() — si algo se corrompe, el backup del día está ahí

-- CONCLUSION

SQLite con el patrón meta JSON elimina la necesidad de migraciones de schema en proyectos que evolucionan rápido. Los campos estructurados van en columnas dedicadas para búsqueda rápida. Los datos flexibles van en el campo meta sin migración. Para una app de escritorio de un solo usuario, este enfoque es suficiente para años de desarrollo sin tocar la estructura de la base de datos.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

APPS DE ESCRITORIO SIN ELECTRON: PYTHON + PYWEBVIEW COMO ALTERNATIVA REAL

TAGS: DESKTOP APPS / PYTHON / ARQUITECTURA READ_TIME: 16 MIN
Apps de escritorio sin Electron: Python + pywebview como alternativa real

Electron tiene un problema que nadie en su ecosistema quiere admitir: consume entre 150MB y 400MB de RAM en reposo, requiere Node.js como runtime, y distribuye un Chromium completo empaquetado en cada instalación. Una app "simple" de escritorio pesa fácilmente 200MB solo por el runtime. Para proyectos donde la lógica principal ya está en Python, existe una alternativa que pocos conocen: pywebview. Mismo resultado visual, fracción del peso, sin Node.js, sin npm.

PROJECT_STATUS: STABLE

Stack: Python 3.10+ · pywebview 4.4 · HTML/CSS/JS
Proyecto de referencia: FocOs v1.3 — Windows + Linux
Objetivo: Eliminar Electron del stack — UI en HTML/CSS/JS, backend en Python puro, bridge directo sin IPC

01. EL PROBLEMA QUE RESUELVE

Electron obliga a duplicar la lógica: la UI en JavaScript/TypeScript y el backend en Node.js con IPC para comunicarse con el sistema. Para proyectos Python, esto significa reescribir todo el backend en Node o mantener dos procesos separados. pywebview elimina esta duplicación: la UI es HTML/CSS/JS, el backend es Python puro, y el bridge entre los dos es directo.

// Explicación no técnica

Imagina que quieres hacer una ventana con una pantalla web adentro. Electron es como construir un edificio entero para poner esa ventana — trae sus propios ladrillos, su propia agua, su propio generador de luz. pywebview es como abrir un hueco en la pared de tu casa y poner la ventana ahí — usa lo que la casa (el sistema operativo) ya tiene. Mucho más liviano, mucho más eficiente.

02. LA COMPARACIÓN HONESTA

Los números son los que toman la decisión. Electron tiene mejor soporte para casos de uso muy específicos — notificaciones nativas, multi-ventana avanzado, DevTools en producción. Fuera de esos casos, pywebview gana en todo lo que importa para un proyecto independiente.

Factor Electron pywebview
Runtime Node.js + Chromium (~200MB) WebView2/WebKit nativo (0MB extra)
Tamaño app 150-300MB mínimo 5-20MB (solo tu código)
RAM en reposo 150-400MB 30-80MB
Backend Node.js obligatorio Python puro
IPC ipcMain / ipcRenderer Bridge directo con js_api
WebEngine Chromium fijo WebView2 (Win) / WebKit (Linux/Mac)
Build electron-builder PyInstaller / Nuitka

03. INSTALACIÓN Y SETUP MÍNIMO

La instalación base es una línea. Las dependencias adicionales dependen del OS — en Windows se necesita el backend WebView2, en Linux el WebKitGTK del sistema.

# Instalación base
pip install pywebview

# Windows — backend WebView2:
pip install pywebview[winforms]
# o para WebView2 explícito:
pip install pywebview[edgechromium]

# Linux — Ubuntu/Debian:
sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0

# Linux — Arch:
sudo pacman -S python-gobject webkit2gtk

# App mínima funcional:
import webview

window = webview.create_window(
    title = 'Mi App',
    url = 'https://example.com',
    width = 1200,
    height = 800,
)
webview.start()

# Cargar HTML local:
from pathlib import Path
url = Path('web/index.html').resolve().as_uri()
window = webview.create_window(title='Mi App', url=url)
webview.start()

04. LA ARQUITECTURA DE FOCOS — ESTRUCTURA REAL

FocOs separa el proyecto en dos dominios claros: el backend Python en la raíz y el frontend completo bajo web/. La base de datos SQLite y la configuración de LLM viven en data/, nunca mezclados con el código de UI.

focos/
├── main.py # Entrada principal + FocOsAPI
├── focos_importer.py # Módulo de importación de proyectos
├── data/ # Base de datos y configuración
│ ├── focos.db # SQLite — proyectos, tareas, workspaces
│ ├── llm_config.json # Configuración de LLM
│ └── projects_config.json
└── web/ # Frontend completo
    ├── index.html # Shell principal
    ├── css/
    │ ├── main.css # Variables y reset
    │ ├── layout.css # Grid y estructura
    │ └── theme.css # Dark-Amber tema
    └── js/
        ├── bridge.js # Abstracción del bridge Python
        ├── llm.js # Panel de LLM
        ├── editor.js # Monaco Editor con pestañas
        ├── terminal.js # Terminal xterm.js
        ├── browser.js # Browser embebido
        └── dashboard.js # Métricas y proyectos

05. LA CLASE FOCOSAPI — EL BRIDGE COMPLETO

Todos los métodos de FocOsAPI son accesibles desde JavaScript via window.pywebview.api.nombre_del_metodo(). Los tipos Python se serializan y deserializan automáticamente — no hay protocolo de mensajes que mantener, no hay tipos que mapear manualmente.

# main.py
import webview, sqlite3, json, os
from pathlib import Path

BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / 'data'
DB_PATH = DATA_DIR / 'focos.db'

class FocOsAPI:

    # -- SISTEMA ------------------------------------------------
    def get_version(self):
        return { 'version': '1.3', 'platform': os.name }

    def open_folder(self, path: str):
        '''Abre una carpeta en el explorador nativo del OS'''
        import subprocess
        if os.name == 'nt':
            subprocess.Popen(['explorer', path])
        else:
            subprocess.Popen(['xdg-open', path])
        return { 'ok': True }

    # -- ARCHIVOS -----------------------------------------------
    def open_file_dialog(self):
        result = window.create_file_dialog(
            dialog_type = webview.FileDialog.OPEN,
            allow_multiple = False,
        )
        if result:
            return { 'ok': True, 'path': str(result[0]) }
        return { 'ok': False }

    def read_file(self, path: str):
        p = Path(path)
        text = p.read_text(encoding='utf-8', errors='replace')
        return { 'ok': True, 'text': text }

    def write_file(self, path: str, content: str):
        Path(path).write_text(content, encoding='utf-8')
        return { 'ok': True }

# -- ARRANQUE -----------------------------------------------
def main():
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    init_db()

    api = FocOsAPI()
    index_url = (BASE_DIR / 'web' / 'index.html').resolve().as_uri()
    global window
    window = webview.create_window(
        title = 'FocOs',
        url = index_url,
        js_api = api,
        width = 1400,
        height = 900,
        min_size = (900, 600),
        resizable = True,
        background_color = '#0A0A0A',
    )
    webview.start(on_start, debug=False)

if __name__ == '__main__':
    main()

06. DISTRIBUIR LA APP — PYINSTALLER

PyInstaller empaqueta el intérprete Python, la app y todos los assets en un único ejecutable. El resultado en Windows es un .exe de ~15MB — sin runtime externo, sin dependencias visibles para el usuario final.

# Instalar PyInstaller
pip install pyinstaller

# Windows — separador ';'
pyinstaller --onefile --windowed \
  --add-data 'web;web' \
  --add-data 'data;data' \
  --name 'FocOs' \
  main.py

# Linux — separador ':'
pyinstaller --onefile --windowed \
  --add-data 'web:web' \
  --add-data 'data:data' \
  --name 'focos' \
  main.py

# Resultado:
dist/
  FocOs.exe # Windows -- ~15MB sin runtime externo
  focos # Linux -- ~12MB

# Incluir icono:
# Windows: --icon=web/assets/focos.ico
# Linux: --icon=web/assets/focos.png
[!] NOTA: WEBVIEW2 EN WINDOWS

pywebview usa WebView2 en Windows. Viene preinstalado en Windows 11. En Windows 10 puede requerir instalación adicional por parte del usuario. Verificar disponibilidad en: developer.microsoft.com/en-us/microsoft-edge/webview2/

07. LIMITACIONES REALES DE PYWEBVIEW

pywebview no es un reemplazo universal de Electron. Hay casos donde Electron sigue siendo la opción correcta. Conocer los límites antes de comprometer la arquitectura evita reescrituras.

Limitación Workaround en Python
DevTools solo con debug=True Activar en desarrollo, desactivar en build de producción.
Multi-ventana con limitaciones Diseñar la app como SPA con paneles — evita la necesidad de ventanas múltiples.
Sin API para notificaciones nativas plyer o notify2 desde Python — independiente de pywebview.
Sin API para system tray / dock pystray desde Python — se integra en el mismo proceso.

-- CONCLUSION

pywebview + Python es la arquitectura correcta para cualquier app de escritorio donde el backend principal ya está en Python. La eliminación de Electron reduce el tamaño de distribución de ~200MB a ~15MB, el consumo de RAM en reposo de ~300MB a ~50MB, y la complejidad del stack de 3 capas a 2 capas directas. FocOs demuestra que es posible construir una app premium de escritorio — con editor de código, terminal, browser y panel LLM — sin una sola línea de Node.js.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

OLLAMA LOCAL: LLMs SIN INTERNET, SIN COSTO, SIN LÍMITE DE CONTEXTO

TAGS: LLMs / PRIVACIDAD / INFRAESTRUCTURA / PYTHON READ_TIME: 12 MIN
Ollama local: LLMs sin internet, sin costo, sin límite de contexto

Groq es rápido. Gemini es capaz. Claude es brillante. Pero todos tienen algo en común: requieren internet, tienen límites de uso, y tus conversaciones pasan por servidores de terceros. Ollama es diferente — es un runtime que corre modelos de lenguaje directamente en tu máquina. Sin internet. Sin API key. Sin costo por token. Tus datos nunca salen de tu disco.

PROJECT_STATUS: STABLE

Stack: Ollama · Python · cualquier OS
Proyecto de referencia: FocOs — proveedor LLM local
Objetivo: Independencia tecnológica total — el LLM corre en tu máquina, los datos nunca salen de tu disco

01. EL PROBLEMA QUE RESUELVE

Las APIs de LLMs tienen tres limitaciones que afectan el flujo de trabajo real: requieren internet estable — sin conexión, sin LLM; tienen límites de rate y contexto que interrumpen sesiones largas; y el código y las ideas que compartes van a servidores externos. Para un desarrollador independiente construyendo su ecosistema propio, estas limitaciones no son teóricas — son obstáculos reales.

Para proyectos largos, para trabajo nocturno, para código sensible, o simplemente para quien valora la independencia — Ollama es el grial.

🎓 Explicación no técnica

Imagina que cada vez que quieres hacerle una pregunta a un experto, tienes que llamarlo por teléfono, esperar que esté disponible, y pagarle por minuto. Eso son las APIs de LLMs. Ollama es como tener ese experto viviendo en tu casa. Siempre disponible. Sin teléfono. Sin cuenta. Sin que nadie más escuche la conversación.

02. INSTALACIÓN — 3 MINUTOS EN CUALQUIER OS

Ollama se instala con un solo comando en Linux, un instalador en Windows, y Homebrew en macOS. Una vez instalado, corre como servicio en background y expone una API REST en localhost:11434.

# LINUX (una línea):
curl -fsSL https://ollama.com/install.sh | sh

# WINDOWS:
# Descargar instalador desde: https://ollama.com/download
# Ejecutar OllamaSetup.exe
# Ollama corre como servicio en background automáticamente

# MACOS:
brew install ollama

# Verificar instalación:
ollama --version
ollama version 0.5.x

# Verificar que el servidor está corriendo:
curl http://localhost:11434/api/tags
# Responde con lista de modelos instalados (vacía al inicio)

03. MODELOS RECOMENDADOS — CUÁL USAR PARA QUÉ

La elección del modelo depende de la RAM disponible y del tipo de tarea. No tiene sentido correr un modelo de 40GB en una máquina de 16GB — la regla es: el modelo más grande que tu hardware corra sin swap.

Modelo RAM mínima Disco Ideal para
llama3.2:3b 8 GB ~2 GB Tareas simples, completar código corto, preguntas rápidas.
llama3.2 8 GB ~5 GB Desarrollo general, debugging, explicaciones técnicas. El equilibrio perfecto.
llama3.3:70b 32 GB ~40 GB Arquitectura compleja, razonamiento profundo, análisis.
gemma2:9b 16 GB ~6 GB Código. Excelente relación capacidad/tamaño. De Google.
codellama 8 GB ~4 GB Completar funciones, debugging técnico, refactoring. Especializado en código.
mistral 8 GB ~4 GB Escritura y síntesis. Rápido y eficiente.
deepseek-r1:8b 8 GB ~5 GB Razonamiento paso a paso. Excelente para problemas lógicos complejos.

04. DESCARGAR Y CORRER UN MODELO

La descarga ocurre una sola vez — el modelo queda guardado localmente y disponible sin conexión de forma permanente. Los comandos de gestión son simples e intuitivos.

# Descargar un modelo (solo la primera vez):
ollama pull llama3.2
# Descarga ~5GB — solo una vez, queda guardado localmente

# Correr en modo chat interactivo:
ollama run llama3.2
# >>> Escribe tu mensaje aquí
# Ctrl+D o /bye para salir

# Correr con prompt directo:
ollama run llama3.2 "explica qué hace este código: def fib(n): return n if n<=1 else fib(n-1)+fib(n-2)"

# Gestión de modelos:
ollama list # ver modelos instalados
ollama ps # ver modelos corriendo
ollama rm llama3.2 # eliminar un modelo
ollama pull llama3.2 # actualizar un modelo

05. INTEGRACIÓN CON PYTHON — LA API REST

Ollama expone una API REST en localhost:11434 compatible con el formato OpenAI. Se integra en FocOs exactamente igual que Groq o Gemini — la capa AAL no distingue si el modelo corre en la nube o en tu disco.

# Llamada directa con urllib — sin dependencias externas
import urllib.request, json

def call_ollama(model, messages, base_url='http://localhost:11434'):
    url = f'{base_url}/api/chat'
    payload = json.dumps({
        'model': model,
        'messages': messages,
        'stream': False,
        'options': {
            'temperature': 0.7,
            'num_ctx': 4096, # Contexto — ajustar según RAM
        }
    }).encode('utf-8')

    req = urllib.request.Request(
        url, data=payload,
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    res = urllib.request.urlopen(req, timeout=120)
    data = json.loads(res.read())
    return data.get('message', {}).get('content', '')

# Verificar disponibilidad antes de llamar:
def ollama_available(base_url='http://localhost:11434'):
    try:
        urllib.request.urlopen(f'{base_url}/api/tags', timeout=2)
        return True
    except Exception:
        return False

# Uso:
response = call_ollama(
    model = 'llama3.2',
    messages = [
        {'role': 'system', 'content': 'Eres un asistente técnico experto.'},
        {'role': 'user', 'content': '¿Cómo implemento un singleton en Python?'}
    ]
)

06. STREAMING — RESPUESTAS EN TIEMPO REAL

Para respuestas largas, el streaming mejora radicalmente la experiencia — el usuario ve el texto aparecer mientras el modelo genera, en lugar de esperar a que termine todo el procesamiento.

def call_ollama_stream(model, messages, on_token):
    '''
    on_token: función que recibe cada fragmento de texto
    Ejemplo: on_token = lambda t: print(t, end='', flush=True)
    '''
    import json, urllib.request

    url = 'http://localhost:11434/api/chat'
    payload = json.dumps({
        'model': model,
        'messages': messages,
        'stream': True, # Activar streaming
    }).encode('utf-8')

    req = urllib.request.Request(
        url, data=payload,
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    full_response = ''
    with urllib.request.urlopen(req, timeout=120) as res:
        for line in res:
            if line.strip():
                chunk = json.loads(line.decode('utf-8'))
                token = chunk.get('message', {}).get('content', '')
                if token:
                    on_token(token)
                    full_response += token
                if chunk.get('done', False):
                    break
    return full_response

# Uso:
call_ollama_stream(
    'llama3.2',
    [{'role': 'user', 'content': 'escribe una función de sorting en Python'}],
    on_token = lambda t: print(t, end='', flush=True)
)

07. MODELOS PERSONALIZADOS — MODELFILE

Ollama permite crear modelos personalizados con un system prompt fijo mediante un Modelfile. Útil para crear el asistente Chronos de FocOs como modelo propio — una vez creado, el contexto del ecosistema está disponible sin inyección manual en cada llamada.

# Modelfile

FROM llama3.2

SYSTEM """
Eres Chronos — asistente de desarrollo de Frank.
Trabajas dentro de FocOs, el window manager del ser que construye.
Conoces el ecosistema: FocOs, TeliOs, KayrOs, ChronOs, OruX.
Metodología: AAL — Arquitectura Agnóstica de LLM.
Stack preferido: Python + HTML/CSS/JS vanilla. Cero dependencias.
Principio: el contrato es la verdad. El código se regenera.
Responde siempre en el idioma del usuario.
Prioriza soluciones prácticas sobre teoría.
Máximo 3 opciones cuando hay alternativas.
"""

PARAMETER temperature 0.7
PARAMETER num_ctx 4096

# Crear el modelo personalizado:
ollama create chronos -f Modelfile

# Usar desde terminal:
ollama run chronos

# Usar desde Python:
response = call_ollama('chronos', [{'role': 'user', 'content': 'hola'}])

✅ CONCLUSIÓN

Ollama convierte la independencia tecnológica de un principio en una realidad práctica. Un desarrollador con Ollama instalado puede construir software con LLMs a las 3 de la madrugada, sin internet, sin gastar un centavo, sin que ningún servidor externo vea su código. La combinación Ollama + FocOs + AAL es el stack más soberano que existe hoy para desarrollo asistido por IA: el LLM corre en tu máquina, el entorno de trabajo es tuyo, la metodología es tuya, y los datos nunca salen de tu disco.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

EL CONTEXTO COMO SUPERPODER: INYECCIÓN AUTOMÁTICA DE ESTADO EN CADA MENSAJE LLM - PARTE I

TAGS: LLMs / PRODUCTIVIDAD / PROMPT ENGINEERING / PYTHON READ_TIME: 12 MIN
El Contexto como Superpoder: Inyección automática de estado en cada mensaje LLM

La diferencia entre un LLM genérico y un asistente que realmente te conoce no está en el modelo. Está en el contexto que le das. Pero escribir el contexto manualmente en cada mensaje es lento, inconsistente y fácil de olvidar. FocOs resuelve esto inyectando automáticamente el contexto del proyecto activo en cada mensaje — sin que el usuario escriba nada extra, sin que se olvide, sin inconsistencias entre sesiones.

PROJECT_STATUS: STABLE

Stack: Python · JavaScript · cualquier LLM
Proyecto de referencia: FocOs — sistema de contexto
Objetivo: Transformar un LLM genérico en un asistente que sabe exactamente dónde estás y qué construyes

01. EL PROBLEMA QUE RESUELVE

Sin contexto automático, el LLM no sabe en qué proyecto estás, qué tarea estás haciendo, qué decisiones se han tomado, ni cuál es el estado actual del sistema. El usuario termina repitiendo el contexto en cada mensaje, lo que consume tokens, tiempo y atención. O peor — no lo hace, y el LLM genera respuestas genéricas que no se aplican al problema específico.

🎓 Explicación no técnica

Imagina que contratas a un consultor experto. Cada vez que lo llamas, tiene amnesia total — no recuerda nada de la última llamada. Tienes dos opciones: explicarle todo desde cero cada vez (agotador), o darle un brief de una página al inicio de cada llamada que lo pone al día en 30 segundos. FocOs genera ese brief automáticamente y lo mete en cada mensaje antes de que llegue al LLM.

02. LA STAMP — EL SELLO DE CONTEXTO

FocOs construye una línea comprimida llamada Stamp que se antepone a cada mensaje. En 80-120 caracteres comunica todo lo que el LLM necesita para responder con precisión.

# Formato de la Stamp:
[F|HH:MM TZ|Día|DX/1096|Proyecto|Tarea|PX|WS]

# Campos:
# F = Identificador del usuario (Frank)
# HH:MM TZ = Hora local con zona horaria
# Día = Día de la semana abreviado
# DX/1096 = Día del KayrOs (cuánto del horizonte ha pasado)
# Proyecto = Nombre del proyecto activo
# Tarea = ID de la tarea o célula activa
# PX = Número de pomodoro en la sesión actual
# WS = Workspace activo (WS1, WS2, WS3)

# Ejemplos reales:
[F|10:30 COT|Jue|D4/1096|TeliOs|TELIO-U003|P1|WS1]
[F|14:45 COT|Mar|D18/1096|FocOs|FOCOS-B012|P3|WS2]
[F|09:00 COT|Lun|D1/1096|Novela-Ciclos|CAP-07|P1|WS1]
[F|22:30 COT|Vie|D365/1096|InfoGraTech|POST-14|P2|WS3]
Campo Ejemplo Descripción
F F Identificador del usuario. Personaliza la identidad del asistente.
HH:MM TZ 10:30 COT Hora local con zona horaria. Permite respuestas sensibles al momento del día.
DX/1096 D4/1096 Día del horizonte KayrOs. Ubica al LLM en el ciclo de vida del proyecto.
Proyecto TeliOs Nombre del proyecto activo en el workspace. Define el dominio de respuesta.
PX P1 Número de pomodoro en la sesión. Indica nivel de fatiga cognitiva del usuario.

03. CONSTRUIR LA STAMP EN PYTHON

La función get_context() en FocOs construye la stamp y el system prompt completo que se inyecta en cada llamada al LLM. Consulta la base de datos local para obtener el estado real del workspace activo.

# main.py — FocOsAPI

def get_context(self) -> dict:
    '''
    Construye el contexto completo del proyecto activo.
    Se inyecta automáticamente en cada llamada al LLM.
    '''
    import datetime
    conn = sqlite3.connect(DB_PATH)

    # Datos del workspace activo
    ws = conn.execute(
        'SELECT project_id FROM workspaces WHERE ws_id=1'
    ).fetchone()
    project_id = ws[0] if ws else None

    # Datos del proyecto
    project = None
    if project_id:
        row = conn.execute(
            'SELECT name, status, meta FROM projects WHERE id=?',
            (project_id,)
        ).fetchone()
        if row:
            project = {
                'name': row[0],
                'status': row[1],
                'meta': json.loads(row[2] or '{}'),
            }

    # Tarea activa
    task = conn.execute(
        'SELECT title FROM tasks WHERE status="active" LIMIT 1'
    ).fetchone()
    conn.close()

    # Construir stamp
    now = datetime.datetime.now()
    hora = now.strftime('%H:%M')
    dia = now.strftime('%a')[:3].capitalize()
    proyecto = project['name'] if project else 'Sin proyecto'
    tarea = task[0][:20] if task else 'Sin tarea'
    day_num = self._get_kayros_day()

    stamp = f'[F|{hora} COT|{dia}|D{day_num}/1096|{proyecto}|{tarea}|P1|WS1]'

    # System prompt con contexto completo
    meta = project['meta'] if project else {}
    system = f'''Eres Chronos — asistente de desarrollo de Frank.
Contexto activo: {stamp}

PROYECTO: {proyecto}
ESTADO: {project["status"] if project else "desconocido"}
TAREA ACTIVA: {tarea}
TIPO: {meta.get("tipo", "general")}
STACK: {meta.get("stack", "no definido")}
FILOSOFÍA: {meta.get("filosofia", "")}

Responde siempre en el idioma del usuario.
Prioriza soluciones prácticas sobre teoría.
Si detectas un error, señálalo antes de responder la pregunta.
Máximo 3 opciones cuando hay alternativas.
'''

    return {
        'stamp': stamp,
        'system': system,
        'proyecto': proyecto,
        'tarea': tarea,
        'day_num': day_num,
    }

04. INYECTAR EL CONTEXTO EN CADA MENSAJE

send_to_llm() llama a get_context() automáticamente. El usuario nunca tiene que escribir el contexto manualmente — llega al LLM en cada mensaje sin intervención.

def send_to_llm(self, message: str, history: list = None) -> dict:
    cfg = self.get_llm_config()
    provider = cfg.get('provider', 'gemini')
    model = cfg.get('model', 'gemini-2.0-flash')

    # CONTEXTO AUTOMÁTICO — siempre presente
    ctx = self.get_context()
    system = ctx['system']
    stamp = ctx['stamp']

    # El mensaje que llega al LLM incluye la stamp
    # EJ: '[F|10:30 COT|Jue|D4/1096|TeliOs|TELIO-U003|P1|WS1]\n\ncómo implemento el bus pub/sub?'
    full_message = f'{stamp}\n\n{message}'

    messages = []
    if history:
        for h in (history if isinstance(history, list) else []):
            if isinstance(h, dict):
                messages.append({
                    'role': h.get('role', 'user'),
                    'content': h.get('content', '')
                })
    messages.append({'role': 'user', 'content': full_message})

    # Despachar al proveedor con system prompt contextualizado
    return self._dispatch_llm(provider, model, system, messages, cfg)

05. EL CONTEXTO PARA PROYECTOS NO TÉCNICOS

La stamp no es exclusiva del desarrollo de software. El mismo mecanismo se adapta a cualquier tipo de proyecto — el system prompt cambia según el dominio, pero la estructura comprimida permanece idéntica.

# ESCRITOR — trabajando en una novela
[F|09:15 COT|Mar|D22/1096|Novela-Ciclos|CAP-07|P1|WS1]

# System prompt adaptado:
# Proyecto: Novela — Ciclos
# Capítulo activo: 7 — El encuentro
# Personaje actual: Elena
# Tono: melancólico, introspectivo
# Última decisión: POV cambia a primera persona en este capítulo

# MÚSICO — produciendo un álbum
[F|23:00 COT|Vie|D45/1096|Album-Raices|TRACK-03|P2|WS1]

# System prompt adaptado:
# Proyecto: Álbum — Raíces
# Track activo: 03 — Sin título aún
# BPM: 92 | Tonalidad: Am | Sección: puente
# DAW: Ableton | Referencia: Nils Frahm

# INVESTIGADOR
[F|11:30 COT|Mié|D8/1096|Tesis-Ecosistemas|CAP-04|P3|WS2]

# System prompt adaptado:
# Proyecto: Tesis — Ecosistemas Digitales
# Capítulo: 4 — Marco metodológico
# Hipótesis activa: H3 — correlación datos/comportamiento
# Fuente pendiente: IEEE 2024 sobre redes complejas
Tipo de proyecto Stamp de ejemplo Contexto clave inyectado
Desarrollo de software D4/1096|TeliOs|TELIO-U003 Stack, arquitectura, estado de la tarea activa.
Escritura / novela D22/1096|Novela-Ciclos|CAP-07 Capítulo, personaje, tono, última decisión narrativa.
Producción musical D45/1096|Album-Raices|TRACK-03 BPM, tonalidad, sección activa, referencia sonora.
Investigación / tesis D8/1096|Tesis-Ecosistemas|CAP-04 Capítulo, hipótesis activa, fuentes pendientes.

06. MOSTRAR LA STAMP EN LA UI DE FOCOS

El bridge de JavaScript expone el contexto al panel LLM en tiempo real. El usuario puede ver en todo momento qué stamp está siendo inyectada y confirmar que el asistente tiene el estado correcto antes de enviar cualquier mensaje.

// bridge.js — Mostrar stamp en el panel LLM

async function llmInit() {
  try {
    const api = await FocOs.ready()
    const ctx = await api.get_context()

    // Mostrar stamp en el header del panel LLM
    const stampEl = document.getElementById('llm-stamp')
    if (stampEl && ctx.stamp) {
      stampEl.textContent = ctx.stamp
      stampEl.title = 'Contexto activo inyectado en cada mensaje'
    }

    // Mostrar proyecto y tarea en el header
    const projEl = document.getElementById('llm-project')
    if (projEl) projEl.textContent = ctx.proyecto

  } catch(e) {
    console.log('llmInit:', e.message)
  }
}

// Cuando el usuario envía un mensaje:
async function llmSend(message) {
  const history = llmGetHistory() // historial de la conversación

  // send_to_llm() inyecta el contexto automáticamente
  // El usuario no tiene que hacer nada
  const result = await FocOs.sendToLLM(message, history)

  if (result.ok) {
    llmAppendMessage('assistant', result.response)
    llmSaveHistory('user', message)
    llmSaveHistory('assistant', result.response)
  } else {
    llmShowError(result.error)
  }
}

CONCLUSIÓN

La inyección automática de contexto transforma un LLM genérico en un asistente que sabe exactamente dónde estás y qué construyes — sin que el usuario escriba nada extra. La stamp de 80 caracteres comunica en formato ultra-comprimido el estado completo de la sesión de trabajo. El mismo mecanismo funciona para cualquier tipo de proyecto: código, escritura, música, investigación. Y porque el contexto se construye desde la base de datos local de FocOs, siempre refleja el estado real — nunca la ficción.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >
> INFOGRATECH_CORE_SHELL X
$