Biblioteca: nuevos edge types ORM y señales Django
PR #39 · cherry-pick #4 de la evaluación Understand-Anything · s79 · 2026-05-22 · autor: @Esquembri
Contexto
El grafo de conocimiento de la Biblioteca se construía hasta ahora con cuatro tipos de edge: imports, inherits, contains y calls. Todos ellos son relaciones estructurales (quién importa a quién, qué función llama a qué). Sin embargo, ninguno capturaba la relación semántica más frecuente en un backend Django: qué funciones leen o escriben en qué modelos, ni qué receivers de señales se disparan al cambiar un modelo.
Este PR incorpora tres nuevos edge types al parser AST (scripts/bib_ast.py), derivados del scope reducido acordado tras la evaluación Understand-Anything (cherry-pick #4 de 9 propuestos). Los cinco restantes (subscribes, publishes, middleware, deploys, serves) requieren parsers no-Python o dependencias de frameworks externos y se posponen.
Nuevos edge types
reads_from — Lecturas ORM
Patrón detectado: <Model>.objects.<METHOD>(...) donde <METHOD> ∈ ORM_READ_METHODS.
Edge: source_qname (función/método) → target_qname (modelo Django)
Métodos cubiertos (20):
| Grupo | Métodos |
|---|---|
| Recuperación básica | filter, get, all, first, last, none |
| Conteo / existencia | count, exists |
| Proyección | values, values_list, only, defer |
| Agregación | aggregate, annotate |
| Optimización | select_related, prefetch_related |
| Ordenación temporal | latest, earliest |
| Bulk / especiales | in_bulk, iterator, raw |
writes_to — Escrituras ORM
Patrón detectado: <Model>.objects.<METHOD>(...) donde <METHOD> ∈ ORM_WRITE_METHODS.
Edge: source_qname (función/método) → target_qname (modelo Django)
Métodos cubiertos (8):
| Método | Semántica |
|---|---|
create | Inserción unitaria |
bulk_create | Inserción masiva |
update | Actualización en queryset |
delete | Eliminación en queryset |
bulk_update | Actualización masiva |
get_or_create | Lookup + inserción condicional |
update_or_create | Upsert |
bulk_create_or_update | Upsert masivo |
triggers — Django signals (@receiver)
Patrón detectado: decorator @receiver(<signal>, sender=<Model>).
Edge: source_qname (función receiver) → target_qname (modelo sender)
Resolución del sender:
sender=MyModel(Name) → se resuelve via imports del módulo.sender="app.Model"(string literal) → se usa como qname directo.@receiver(signal)sinsender=→ no se crea edge (señal global, sin modelo concreto asociado).
Convención semántica: el edge va de la función receiver → modelo, en la dirección “función que se dispara desde el modelo”. Se sigue la convención Understand-Anything: el receiver “triggers” su lógica desde eventos del modelo.
Implementación técnica
El cambio se concentra íntegramente en scripts/bib_ast.py (+104 LOC):
scripts/bib_ast.py
├── ORM_READ_METHODS (set, 20 strings)
├── ORM_WRITE_METHODS (set, 8 strings)
├── SIGNAL_DECORATOR_NAMES (set, {"receiver"})
├── visit_Call() [extendido] → detecta X.objects.METHOD
└── _visit_function() [extendido] → detecta @receiver(...sender=...)
Lógica visit_Call (reads_from / writes_to):
- Se obtiene el callee como string con puntos (
_call_target). - Se splitea por
.→ se verificaparts[-2] == "objects"ylen(parts) >= 3. parts[0](alias del modelo) se resuelve via_resolve_name.parts[-1](método) se clasifica enORM_READ_METHODS/ORM_WRITE_METHODS.- Se emite edge con
source_qname = _current_func.
Lógica _visit_function (triggers):
- Se itera
node.decorator_list. - Se verifica
isinstance(dec, ast.Call)y nombre enSIGNAL_DECORATOR_NAMES. - Se busca keyword
arg="sender". - Se resuelve el valor (Constant string o Name).
- Se emite edge con
source_qname = módulo.función(oclase.método).
Resultado en corpus
Corpus completo: 340 archivos .py, 7 apps.
| Métrica | Valor |
|---|---|
| Edges pre-cambio | ~5.984 |
| Edges post-cambio | ~6.644 |
| Nuevos edges totales | +660 |
— reads_from | 516 |
— writes_to | 143 |
— triggers | 1 |
| Ratio enriquecimiento | ~10% |
| Falsos positivos (sample manual) | 0 |
Scope descartado (iteración futura)
Los otros 5 edge types propuestos por Understand-Anything se descartaron por complejidad de parser o dependencias externas:
| Edge type | Motivo del descarte |
|---|---|
subscribes / publishes | Requiere parser de Django Channels / Huey |
middleware | Sensible a configuración de framework externo |
deploys / serves | Requiere parsers Dockerfile / docker-compose.yml |
Se retoman si emergen consultas concretas del Oráculo que los necesiten.
Riesgo y rollback
Riesgo: BAJO. El cambio solo añade edges nuevos, no modifica ni elimina nodos ni edges existentes.
Impacto de fallo en cron Hetzner:
- El grafo queda obsoleto pero no se corrompe.
- Oráculo y Help Widget siguen sirviendo con datos pre-cambio.
Procedimiento de rollback:
# Restaurar backup pre-cherry-pick (s79)
rsync /opt/biblioteca-crons-bak-20260522-pre-cherrypick/ /opt/biblioteca-crons/
# + revert del commit de este PR en main
git revert <SHA>
Backup D1 adicional disponible en C:\dev\backups\d1-pre-cherrypick.
Véase también
- [[decision—20260522—evaluacion-understand-anything-vs-bibliotecario]]