Sistema de monitoreo de cambios en archivos de configuración de agentes IA, con diffing semántico, clasificación de severidad y rendering en Markdown/JSON.

Estado: Funcional desde v0.3.0 (Fase 3 completada).


Visión general

El sistema de changelog responde a la pregunta: ¿qué cambió en la configuración de los agentes IA? Monitorea archivos como CLAUDE.md, .cursorrules, AGENTS.md y configs YAML/JSON a través del historial de git, produciendo diffs semánticos con clasificación de severidad.

licit changelog
# Agent Config Changelog

> 3 change(s) detected across 2 file(s): **1** major, **1** minor, **1** patch

## .architect/config.yaml

- **[MAJOR]** Changed: model from claude-sonnet-4 to claude-opus-4 (`a1b2c3d4`) — 2026-03-12
- **[PATCH]** Changed: budget.max_cost_usd from 5.0 to 10.0 (`a1b2c3d4`) — 2026-03-12

## CLAUDE.md

- **[MINOR]** Changed: section:Rules from 5 lines to 8 lines (+3/-0) (`e5f6g7h8`) — 2026-03-11

Arquitectura

El sistema se compone de 4 módulos en src/licit/changelog/:

changelog/
├── watcher.py       # Monitoreo de archivos via git history
├── differ.py        # Diffing semántico por formato de archivo
├── classifier.py    # Clasificación de severidad (MAJOR/MINOR/PATCH)
└── renderer.py      # Rendering en Markdown o JSON

Pipeline

ConfigWatcher ──→ Semantic Differ ──→ Change Classifier ──→ Renderer
  (git log)       (YAML/JSON/MD)     (MAJOR/MINOR/PATCH)   (MD/JSON)
       │                │                     │                  │
  ConfigSnapshot[]   FieldDiff[]        ConfigChange[]      String output

Config Watcher

ConfigWatcher monitorea archivos de configuración de agentes IA a través del historial de git.

ConfigSnapshot

@dataclass
class ConfigSnapshot:
    path: str          # Ruta relativa del archivo
    content: str       # Contenido del archivo en ese commit
    commit_sha: str    # Hash SHA del commit
    timestamp: datetime # Fecha del commit (timezone-aware)
    author: str        # Autor del commit

Uso

from licit.changelog.watcher import ConfigWatcher

watcher = ConfigWatcher(root_dir="/path/to/project", watch_patterns=["CLAUDE.md", "*.yaml"])

# Archivos que existen actualmente en disco
files = watcher.get_watched_files()

# Historial de cambios de todos los archivos watched
history = watcher.get_config_history()
# → {"CLAUDE.md": [ConfigSnapshot, ...], "config.yaml": [...]}

# Historial desde una fecha
history = watcher.get_config_history(since="2026-01-01")

Resolución de patrones

Los watch_patterns se resuelven de dos formas:

TipoEjemploResolución
Nombre exactoCLAUDE.mdVerifica existencia en git history (git log --oneline -1)
Glob.prompts/**/*.mdResuelve con Path.glob() y filtra archivos existentes

Protecciones


Semantic Differ

diff_configs() produce diffs semánticos según el formato del archivo.

FieldDiff

@dataclass
class FieldDiff:
    field_path: str           # "model", "llm.provider", "section:Rules"
    old_value: str | None     # Valor anterior (None si es adición)
    new_value: str | None     # Valor nuevo (None si es eliminación)
    is_addition: bool = False # Campo nuevo
    is_removal: bool = False  # Campo eliminado

Formatos soportados

FormatoExtensionesEstrategia
YAML.yaml, .ymlDict recursivo key-value con _diff_dicts()
JSON.jsonDict recursivo key-value con _diff_dicts()
Markdown.mdSecciones por headings con _parse_md_sections()
Texto planoOtrosDiff de contenido completo

YAML / JSON

Parsea ambas versiones, luego diff recursivo de diccionarios:

diffs = diff_configs("model: gpt-4\ntemp: 0.7\n", "model: gpt-5\ntemp: 0.7\n", "config.yaml")
# → [FieldDiff(field_path="model", old_value="gpt-4", new_value="gpt-5")]

Dicts anidados se recurren:

diffs = diff_configs("llm:\n  model: gpt-4\n", "llm:\n  model: gpt-5\n", "config.yaml")
# → [FieldDiff(field_path="llm.model", old_value="gpt-4", new_value="gpt-5")]

Roots no-dict (listas, escalares) se wrappean como {"(root)": data} en vez de descartarse.

Errores de parseo producen FieldDiff(field_path="(parse-error)") sin crashear.

Markdown

Parsea headings ATX (#, ##, ###, etc.) y produce diffs por sección:

old = "# Rules\n\nOriginal rules\n"
new = "# Rules\n\nModified rules\n\n## New Section\n\nContent\n"
diffs = diff_configs(old, new, "CLAUDE.md")
# → [FieldDiff(field_path="section:Rules", ...), FieldDiff(field_path="section:New Section", ...)]

Fenced code blocks: _parse_md_sections() trackea bloques ``` para no interpretar headings dentro de código.

Sin headings: Si el markdown no tiene headings, se cae a diff de contenido completo como (content).

Texto plano

Para archivos como .cursorrules:

diffs = diff_configs("line1\nline2\n", "line1\nline3\n", ".cursorrules")
# → [FieldDiff(field_path="(content)", old_value="2 lines", new_value="2 lines (+1/-1)")]

Change Classifier

ChangeClassifier asigna severidad a cada FieldDiff y produce ConfigChange.

Reglas de severidad

SeveridadTriggerEjemplos
MAJORCampo en _MAJOR_FIELDSmodel, llm.model, provider, backend
MINORCampo en _MINOR_FIELDSprompt, guardrails, tools, rules, blocked_commands
MAJOR (escalación)Eliminación de campo MINORBorrar guardrails, borrar protected_files
MINORCambio en sección Markdownsection:Rules, section:Instructions
PATCHTodo lo demásTweaks de parámetros, formatting, comentarios

Matching por segmentos

_field_matches() compara los últimos N segmentos del campo contra el patrón:

_field_matches("llm.model", "model")       # True  — último segmento = "model"
_field_matches("model", "model")            # True  — segmento único coincide
_field_matches("model_config", "model")     # False — "model_config" ≠ "model"
_field_matches("section:model", "model")    # False — "section:model" ≠ "model"
_field_matches("llm.model", "llm.model")    # True  — últimos 2 segmentos coinciden

Esto previene falsos positivos donde campos como model_config se clasificaban erróneamente como MAJOR.

Uso

from licit.changelog.classifier import ChangeClassifier

classifier = ChangeClassifier()
changes = classifier.classify_changes(
    old_content="model: gpt-4\n",
    new_content="model: gpt-5\n",
    file_path="config.yaml",
    commit_sha="abc1234",
    timestamp=datetime(2026, 3, 10, tzinfo=UTC),
)
# → [ConfigChange(severity=MAJOR, description="Changed: model from gpt-4 to gpt-5", ...)]

Changelog Renderer

ChangelogRenderer convierte una lista de ConfigChange en Markdown o JSON.

Markdown

from licit.changelog.renderer import ChangelogRenderer

renderer = ChangelogRenderer()
output = renderer.render(changes, fmt="markdown")

Estructura del output:

  1. Header # Agent Config Changelog
  2. Summary: N change(s) across M file(s): X major, Y minor, Z patch
  3. Secciones por archivo (ordenadas alfabéticamente)
  4. Dentro de cada archivo: ordenado por severidad (MAJOR primero), luego timestamp descendente
  5. Footer con timestamp UTC de generación

JSON

output = renderer.render(changes, fmt="json")

Produce:

{
  "changes": [
    {
      "file_path": "config.yaml",
      "field_path": "model",
      "old_value": "gpt-4",
      "new_value": "gpt-5",
      "severity": "major",
      "description": "Changed: model from gpt-4 to gpt-5",
      "timestamp": "2026-03-10T00:00:00+00:00",
      "commit_sha": "abc1234"
    }
  ]
}

ensure_ascii=False para soporte completo de Unicode (ñ, ü, 日本語, etc.).


Configuración

changelog:
  enabled: true
  watch_files:
    - CLAUDE.md
    - .cursorrules
    - .cursor/rules
    - AGENTS.md
    - .github/copilot-instructions.md
    - .github/agents/*.md
    - .architect/config.yaml
    - architect.yaml
  output_path: .licit/changelog.md
CampoTipoDefaultDescripción
enabledbooltrueHabilitar monitoreo
watch_fileslist[str](8 patrones)Archivos/globs a monitorear
output_pathstr.licit/changelog.mdRuta del changelog generado

Integración con compliance

El changelog de configuraciones de agentes alimenta directamente el EvidenceBundle:

Campo del bundleQué aporta changelog
has_changelogTrue si existe changelog generado
changelog_entry_countNúmero de entradas en el changelog

Estos campos son evaluados por los frameworks de compliance:


Testing

93 tests cubren el sistema de changelog:

MóduloTestsArchivo
Watcher12tests/test_changelog/test_watcher.py
Differ19tests/test_changelog/test_differ.py
Classifier22tests/test_changelog/test_classifier.py
Renderer10tests/test_changelog/test_renderer.py
Integration3tests/test_changelog/test_integration.py
QA Edge Cases27tests/test_changelog/test_qa_edge_cases.py
Total93

Los tests incluyen: