# ComfyUI Skill — Recetas versionadas de generación reutilizables

Tag: `comfyui-skill`. Grupo para tratar una configuración de generación de ComfyUI como una
**skill**: una receta versionada en disco (checkpoint + LoRAs + params + scaffold de prompt +
bloques de post-proceso) que se guarda una vez y se reproduce a un workflow concreto cambiando
solo el *subject*. Es la doctrina del issue 0087 aplicada a la generación de imágenes: el registry
crece **promoviendo configuraciones que funcionan a recetas reutilizables**, no reescribiendo el
grafo de nodos cada vez.

Construye sobre el grupo [`comfyui`](comfyui.md) (los builders puros de workflow y el ciclo
submit/wait). Una skill no es un workflow: es la *receta* que compila a uno.

Filtro MCP: `mcp__registry__fn_search query="" tag="comfyui-skill"`.

> **Tamaño del grupo (al 28/06/2026):** 17 funciones con tag `comfyui-skill` — CRUD de recetas
> (save/load/list), compilación a workflow (`build_skill_workflow`), inyectores encadenables
> (`inject_hires_fix`/`inject_multi_lora`, `build_ipadapter_workflow`), bucle de mejora
> genera→juzga→bump (`generate_with_skill_oneshot` + `update_skill_score` + `bump_skill_version`),
> export a grafo (`export_skill_template`), mixer de capacidades (`compose_capabilities` +
> `generate_mixed_oneshot`) y cosecha de Civitai (`extract_recipe_from_png` + `harvest_civitai_skill_oneshot`).

## Qué es una skill

Una receta vive en `~/ComfyUI/skills_library/<slug>/` y la manipulan las funciones de este grupo:

```
~/ComfyUI/skills_library/
  INDEX.md                      # índice regenerado de todas las skills
  <slug>/
    recipe.json                 # la receta actual
    versions/vN.json            # snapshot inmutable de cada save (N incremental)
    growth_log.jsonl            # bitácora append-only de cada save
    exports/                    # plantillas de workflow exportadas
    samples/                    # imágenes de muestra
```

### Schema de `recipe.json` (canónico)

```json
{
  "schema_version": 1,
  "slug": "portrait_cinematic_sdxl",
  "version": "1.0.0",
  "title": "Retrato cinematográfico SDXL",
  "base_workflow": "txt2img",
  "checkpoint": "juggernaut_xl_v11.safetensors",
  "loras": [{"name": "add_detail.safetensors", "strength_model": 0.6, "strength_clip": 0.6}],
  "params": {"steps": 30, "cfg": 5.5, "sampler_name": "dpmpp_2m",
             "scheduler": "karras", "width": 832, "height": 1216, "denoise": 1.0},
  "prompt_scaffold": {"positive": "cinematic portrait of {subject}, sharp focus",
                      "negative": "blurry, lowres", "trigger_words": []},
  "blocks": [{"type": "facedetailer", "params": {"denoise": 0.45}},
             {"type": "hires_fix", "params": {"upscale_by": 1.5, "denoise": 0.4}}],
  "score_mean": 0.0, "score_n": 0,
  "provenance": {"source": "manual", "nsfw": false},
  "export_template_path": "exports/portrait_cinematic_sdxl.template.json"
}
```

`base_workflow` ∈ {`txt2img`, `flux`, `sdxl_refiner`} (las bases que se generan desde un *subject*
de texto). `blocks[].type` ∈ {`facedetailer`, `hires_fix`}.

## Funciones del grupo

| ID | Firma corta | Qué hace | Purity |
|---|---|---|---|
| [comfyui_build_skill_workflow_py_ml](../../python/functions/ml/comfyui_build_skill_workflow.md) | `build_skill_workflow(recipe, subject, *, seed=0) -> dict` | Compila una receta a un workflow en API format: despacha al builder base, sustituye `{subject}` + trigger_words, encadena LoRAs y aplica los blocks en orden. `SkillWorkflowError` si la base es desconocida o requiere imagen. | **pura** |
| [comfyui_export_skill_template_py_ml](../../python/functions/ml/comfyui_export_skill_template.md) | `export_skill_template(slug, *, ui_graph=False, port=9222, ...) -> dict` | Exporta una skill a artefactos cargables como GRAFO: template API en `exports/<slug>.template.json` y, con `ui_graph=True`, el UI graph posicionado (vía `load_workflow_ui`+`export_workflow_ui` por CDP) en la carpeta nativa `~/ComfyUI/user/default/workflows/<slug>.json` (menú Workflows del navegador). Sin navegador, deja el template API y reporta el fallback. | impura |
| [comfyui_inject_hires_fix_py_ml](../../python/functions/ml/comfyui_inject_hires_fix.md) | `comfyui_inject_hires_fix(workflow, *, upscale_by=1.5, denoise=0.4, steps=20, ...) -> dict` | Inyecta una 2ª pasada hires-fix (UpscaleModelLoader + UltimateSDUpscale) sobre un workflow ya construido, repuntando el SaveImage. Versión encadenable-sobre-dict del builder hermano. | **pura** |
| [comfyui_inject_multi_lora_py_ml](../../python/functions/ml/comfyui_inject_multi_lora.md) | `comfyui_inject_multi_lora(workflow, loras) -> dict` | Encadena N `LoraLoader` sobre un workflow ya construido reusando `comfyui_inject_lora` por LoRA. Cada lora = `{name, strength_model, strength_clip}`; respeta el orden (primero cerca del checkpoint, último cerca del KSampler). Apila estilo + detalle en una sola llamada. | **pura** |
| [comfyui_build_ipadapter_workflow_py_ml](../../python/functions/ml/comfyui_build_ipadapter_workflow.md) | `comfyui_build_ipadapter_workflow(prompt, ref_image, *, base_checkpoint, mode='style'\|'faceid', weight=0.8, ...) -> dict` | txt2img + IPAdapter (custom node cubiq). `mode='style'` transfiere estilo/composición de una imagen de referencia (IPAdapterUnifiedLoader+IPAdapter); `mode='faceid'` impone un rostro consistente vía insightface + .bin FaceID + su LoRA (IPAdapterUnifiedLoaderFaceID+IPAdapterFaceID). Repunta el KSampler a la rama IPAdapter. | **pura** |
| [comfyui_save_skill_py_ml](../../python/functions/ml/comfyui_save_skill.md) | `comfyui_save_skill(recipe, *, library_dir=None) -> dict` | Valida el schema mínimo y escribe `recipe.json` + snapshot `versions/vN.json` + growth_log + INDEX.md. No muta la receta (round-trip con load). | impura |
| [comfyui_load_skill_py_ml](../../python/functions/ml/comfyui_load_skill.md) | `comfyui_load_skill(slug, *, version=None, library_dir=None) -> dict` | Lee `recipe.json` (actual) o un snapshot `versions/vN.json`. Slug/versión inexistente → `{ok:False}` sin lanzar. | impura |
| [comfyui_list_skills_py_ml](../../python/functions/ml/comfyui_list_skills.md) | `comfyui_list_skills(*, library_dir=None, include_nsfw=False) -> dict` | Lista las skills con slug/title/base_workflow/version/score/nsfw/n_versions. Oculta NSFW por defecto. | impura |
| [ask_llm_vision_py_core](../../python/functions/core/ask_llm_vision.md) | `ask_llm_vision(prompt, image_path='', *, image_b64='', media_type='', model='claude-opus-4-8', ...) -> dict` | Pregunta multimodal (imagen + texto) al modelo via API directa de Anthropic (grupo `claude-direct`). Útil para **puntuar** el PNG de una skill y alimentar `score_mean`. | impura |
| [comfyui_generate_with_skill_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_generate_with_skill_oneshot.md) | `generate_with_skill_oneshot(slug, subject, *, server='127.0.0.1:8188', dest=None, seed=0, judge=True, recipe_patch=None, ...) -> dict` | One-shot del bucle: carga la skill, la compila para el `subject`, encola, espera, descarga el PNG y (si `judge`) lo puntúa con el panel `comfyui-judge`, acumulando el score en la media. `recipe_patch` prueba una variante en memoria sin guardar. | pipeline (impura) |
| [comfyui_update_skill_score_py_ml](../../python/functions/ml/comfyui_update_skill_score.md) | `comfyui_update_skill_score(slug, new_score, *, library_dir=None) -> dict` | Acumula el score de un juicio en `score_mean`/`score_n` por media incremental, reescribiendo `recipe.json` en sitio (sin snapshot ni growth_log). | impura |
| [comfyui_bump_skill_version_py_ml](../../python/functions/ml/comfyui_bump_skill_version.md) | `comfyui_bump_skill_version(slug, change, *, score_before, score_after, judge_run_id=None, recipe_patch=None, force=False, ...) -> dict` | Promueve una versión nueva **solo si el score sube** (gate objetivo): snapshot `versions/vN.json` + aplica `recipe_patch` + sube el semver + línea en `growth_log`. Gate bloquea si no mejora. | impura |
| [comfyui_extract_recipe_from_png_py_ml](../../python/functions/ml/comfyui_extract_recipe_from_png.md) | `comfyui_extract_recipe_from_png(png_path, *, slug=None, civitai_meta=None, image_url='', nsfw=False) -> dict` | Destila un PNG cosechado de Civitai en una receta de skill **candidata** (`score_n=0`, `provenance.source='civitai'`). Compone `comfyui_import_workflow_png` (workflow API embebido) + `comfyui_read_png_metadata` (params del KSampler); fallback a la `meta` de Civitai. Degradación honesta: `ok=False` sin inventar si no hay ni workflow embebido ni meta utilizable. | impura |
| [comfyui_harvest_civitai_skill_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_harvest_civitai_skill_oneshot.md) | `comfyui_harvest_civitai_skill_oneshot(*, query=None, model_version_id=None, nsfw='None', dest_dir, library_dir='~/ComfyUI/skills_library', ...) -> dict` | One-shot Civitai → skill candidata: `search` → `fetch` (segrega NSFW) → `extract_recipe` → `save_skill`. Itera los items hasta hallar uno con receta destilable (preferentemente workflow embebido), descartando los PNG sin receta; 2º pase al feed global si filtró por modelo. **No baja modelos a ciegas**: los ausentes van a `missing_models`. | pipeline (impura) |

`build_skill_workflow` compone los builders del grupo [`comfyui`](comfyui.md):
`comfyui_build_txt2img_workflow`, `comfyui_build_flux_workflow`,
`comfyui_build_sdxl_refiner_workflow`, `comfyui_inject_lora`,
`comfyui_build_facedetailer_workflow` y `comfyui_inject_hires_fix`.

## Ejemplo canónico end-to-end (receta → workflow → PNG → score)

Guardar una skill, cargarla, compilarla a un workflow para un sujeto, encolarla y puntuar el
resultado con visión. Requiere el server ComfyUI en `127.0.0.1:8188` y los modelos de la receta
instalados.

```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_save_skill import comfyui_save_skill
from ml.comfyui_load_skill import comfyui_load_skill
from ml.comfyui_build_skill_workflow import build_skill_workflow
from ml.comfyui_submit_workflow import comfyui_submit_workflow
from ml.comfyui_wait_result import comfyui_wait_result
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
from core.ask_llm_vision import ask_llm_vision

# 1. Definir y guardar la skill (una vez).
recipe = {
    "schema_version": 1, "slug": "portrait_cinematic_sdxl", "version": "1.0.0",
    "title": "Retrato cinematográfico SDXL", "base_workflow": "txt2img",
    "checkpoint": "dreamshaper_8.safetensors",
    "loras": [{"name": "add_detail.safetensors", "strength_model": 0.6, "strength_clip": 0.6}],
    "params": {"steps": 28, "cfg": 6.0, "sampler_name": "dpmpp_2m", "scheduler": "karras"},
    "prompt_scaffold": {"positive": "cinematic portrait of {subject}, sharp focus",
                        "negative": "blurry, lowres", "trigger_words": []},
    "blocks": [{"type": "facedetailer", "params": {"denoise": 0.45}}],
    "score_mean": 0.0, "score_n": 0, "provenance": {"source": "manual", "nsfw": False},
}
comfyui_save_skill(recipe)                                 # ~/ComfyUI/skills_library/portrait_cinematic_sdxl/

# 2. Cargar + compilar a un workflow para un sujeto concreto.
recipe = comfyui_load_skill("portrait_cinematic_sdxl")["recipe"]
wf = build_skill_workflow(recipe, "a woman with red hair", seed=42)

# 3. Encolar y esperar el PNG (camino headless del grupo comfyui).
pid = comfyui_submit_workflow(wf)["prompt_id"]
outputs = comfyui_wait_result(pid)["outputs"]
img = comfyui_fetch_output_image(outputs[0]["filename"], dest_dir="/tmp")["path"]

# 4. Puntuar el resultado con visión (alimenta el bucle de scoring de la skill).
verdict = ask_llm_vision(
    "Puntúa de 0 a 10 el realismo de este retrato. Responde solo el número.",
    image_path=img, model="claude-opus-4-8",
)
print(verdict["text"])
```

El paso "guardar la receta" se hace una sola vez; a partir de ahí cada generación es
`load → build → submit`, cambiando solo el `subject` y la `seed`.

## Bucle de mejora (skill → genera → juzga → bump)

La doctrina del issue 0087 cerrada en un lazo: una skill **no crece inflando la receta a ciegas,
crece registrando mejoras medibles**. El juez (no el humano) decide qué se promueve.

```
   ┌─────────────────────────────────────────────────────────────────┐
   │  generate_with_skill_oneshot(slug, subject, judge=True)          │
   │     load → build → submit → wait → fetch → judge → score_mean    │  ← canónica
   └─────────────────────────────────────────────────────────────────┘
            │  score_before = score de la receta vigente
            ▼
   ┌─────────────────────────────────────────────────────────────────┐
   │  generate_with_skill_oneshot(..., recipe_patch={params:{...}})   │  ← variante (no guarda score)
   │     misma seed, un cambio plausible → judge.score = score_after  │
   └─────────────────────────────────────────────────────────────────┘
            │
            ▼  GATE objetivo
   comfyui_bump_skill_version(slug, change, score_before, score_after, judge_run_id=...)
            │
      score_after > score_before ?
        ├── sí → promueve: versions/vN.json (snapshot) + recipe_patch + semver↑ + growth_log
        └── no → {ok:False} — NO se promueve (la variante se descarta)
```

Pasos concretos:

1. **Genera la canónica** con `judge=True`. El panel `comfyui-judge` emite un `score` y el pipeline
   lo acumula en `score_mean`/`score_n` de la skill (vía `comfyui_update_skill_score`). Ese score es
   el `score_before`.
2. **Genera una variante** con `recipe_patch` (p.ej. `{"params": {"steps": 32}}`) y la **misma seed**.
   El patch se aplica en memoria, NO se guarda, y su score NO contamina la media. Su `judge.score` es
   el `score_after`, y su `judge_run_id` es la evidencia.
3. **Promueve con el gate**: `comfyui_bump_skill_version` aplica el patch a `recipe.json`, sube el
   semver y deja una línea en `growth_log.jsonl` **solo si `score_after > score_before`**. Si no
   mejora, devuelve `{ok:False}` y la receta se queda como estaba. El gate es objetivo: lo decide el
   número del juez, no quien lanza la generación.

Así `versions/` y `growth_log` reflejan **versiones de receta con mejora demostrada**, mientras
`score_mean` es la telemetría de calidad media de la versión vigente.

## Skills como grafos en el navegador

Una skill no vive solo como receta JSON: se exporta a un **grafo de ComfyUI cargable como tal en el
navegador**. `comfyui_export_skill_template` cierra ese hueco (receta → grafo):

```python
from ml.comfyui_export_skill_template import export_skill_template

# Headless (sin navegador): congela el template API junto a la skill.
export_skill_template("portrait_cinematic_sd15")
# -> exports/portrait_cinematic_sd15.template.json  (API format, node-template reproducible)

# Con navegador (pestaña ComfyUI abierta en CDP 9222): además el grafo visual posicionado.
out = export_skill_template("portrait_cinematic_sd15", ui_graph=True, port=9222)
# -> ~/ComfyUI/user/default/workflows/portrait_cinematic_sd15.json  (aparece en el menú Workflows)
```

Dos formatos, dos usos:

- **API format** (`exports/<slug>.template.json`) — el dict `{node_id:{class_type,inputs}}`. Se
  carga con `comfyui_load_workflow_ui` (`app.loadApiJson`, litegraph lo auto-posiciona) o va directo
  a `comfyui_submit_workflow`. Es el node-template versionable de la skill.
- **UI graph** (`~/ComfyUI/user/default/workflows/<slug>.json` + copia en `exports/<slug>.ui.json`)
  — `nodes`/`links`/`pos` (`app.graph.serialize()`). La carpeta nativa de la UI **solo** acepta este
  formato; por eso solo se escribe con `ui_graph=True` (se genera vía CDP cargando el API en la UI y
  serializando el grafo posicionado). Es el que se abre como grafo visual desde el menú Workflows.

**Fotos ↔ grafo.** Cada PNG de ComfyUI lleva su workflow embebido (chunk `prompt`, API format).
`comfyui_import_workflow_png` lo recupera, de modo que toda muestra de una skill queda asociada a su
grafo reproducible 1:1 (ver `INDEX.md` de la librería: `samples/<base>.png` + `samples/<base>.graph.json`).

**No destructivo en el navegador**: `ui_graph=True` reemplaza el grafo in-memory de la pestaña. Si
hay trabajo sin guardar (título con `*`), respalda antes con
`comfyui_export_workflow_ui(api_format=True, save_path=...)` y restáuralo después con
`comfyui_load_workflow_ui`.

## Cosecha Civitai → skill candidata

El registry crece también captando recetas que **ya existen en internet**, no solo escribiéndolas a
mano: doctrina del issue 0087 aplicada a la captación de assets. Cada imagen publicada en Civitai
suele llevar su workflow de ComfyUI embebido en el PNG (chunk `prompt`, API format); cosecharla
destila la receta entera (checkpoint + LoRAs + params + prompt) en una **skill candidata** lista para
juzgar.

```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions", "pipelines"))
from comfyui_harvest_civitai_skill_oneshot import comfyui_harvest_civitai_skill_oneshot

res = comfyui_harvest_civitai_skill_oneshot(
    query="cinematic portrait", nsfw="None",
    dest_dir=os.path.expanduser("~/ComfyUI/civitai_harvest"),
)
print(res["ok"], res["slug"], "workflow_embebido=", res["has_workflow"])
print("guardada en:", res["skill_path"])           # ~/ComfyUI/skills_library/<slug>/recipe.json
print("modelos ausentes:", res["missing_models"])  # checkpoints/LoRAs a bajar (NO bajados)
```

Dos piezas, una composición:

- **`comfyui_extract_recipe_from_png`** (paso puro de destilación, impura solo por leer disco) —
  toma un PNG ya descargado y produce la receta candidata. Compone
  `comfyui_import_workflow_png` (workflow embebido) + `comfyui_read_png_metadata` (params del
  KSampler), con fallback a la `meta` de generación de Civitai y, para samplers no-KSampler
  (flux/`SamplerCustomAdvanced`), heurística sobre los nodos `CLIPTextEncode`. **Degradación honesta**:
  si no hay ni workflow embebido ni meta utilizable devuelve `ok=False` sin inventar la receta.
- **`comfyui_harvest_civitai_skill_oneshot`** (pipeline) — encadena
  `search_civitai_images → fetch_civitai_image → extract_recipe_from_png → save_skill` en una sola
  llamada. Itera los resultados del search hasta encontrar el primero con receta destilable
  (descartando los PNG sin workflow), y si filtró por un modelo concreto y ninguno trae grafo, hace
  un 2º pase al feed global "Most Reactions" (donde abundan los workflows ComfyUI de usuarios flux).

Notas de uso:

- **La skill nace CANDIDATA** (`score_n=0`, `provenance.source='civitai'`): no está validada. El
  prompt cosechado es **concreto**, no un scaffold con `{subject}` — sustitúyelo a mano si quieres
  reutilizar la skill para otros sujetos. La validación la da el bucle
  `generate_with_skill_oneshot` (juzga) + `comfyui_bump_skill_version` (promueve si mejora).
- **No baja modelos a ciegas**: si la receta referencia un checkpoint o LoRA que no está en
  `<comfyui_dir>/models/`, lo lista en `missing_models` y no descarga nada. Bajarlos
  (`comfyui_search_civitai_models` + `comfyui_download_model`) es una decisión aparte del caller.
- **NSFW segregado**: el PNG se descarga a `<dest_dir>/nsfw/` si el item es NSFW (permitido pero
  siempre separado). El `dest_dir` vive fuera del repo (`~/ComfyUI/`) y se trata como datos: no se
  commitea ni se indexa.
- El token Civitai es secreto: viene de `pass civitai/api-token`, nunca hardcodeado.

## Mezclar capacidades (mixer)

Una skill fija *una* receta. El **mixer** resuelve el otro eje: combinar **a la carta** todas las
capacidades de generación sobre un mismo workflow base y activar/desactivar cada una para iterar.
Misma doctrina del issue 0087 (componer piezas probadas, no reescribir el grafo), pero aplicada a
mezclar capacidades en vez de a guardar una receta.

Dos funciones:

| ID | firma corta | qué hace |
|---|---|---|
| `comfyui_compose_capabilities_py_ml` | `compose_capabilities(base, *, loras, controlnet, ipadapter, hires, facedetailer) -> dict` | **PURA.** Aplica EN ORDEN las capacidades activadas (cada arg `None` = desactivada) sobre un dict base, componiendo los inyectores/builders encadenables. Reconecta MODEL/CLIP/positive/IMAGE. Sin ninguna = base intacto. |
| `comfyui_generate_mixed_oneshot_py_pipelines` | `generate_mixed_oneshot(base, subject, *, capabilities, server, judge, ...) -> dict` | **Pipeline.** base (skill slug / `'txt2img'` / dict) → compose → submit → wait → fetch → (si `judge`) juzga. Devuelve `{ok, prompt_id, image_path, capabilities_active, judge, error}`. |

El mixer se apoya en los **inyectores encadenables-sobre-dict** (cada uno la versión componible de
su builder-desde-cero hermano):

| Capacidad | Inyector | Reconecta |
|---|---|---|
| LoRAs (N) | `comfyui_inject_multi_lora_py_ml` | cadena MODEL/CLIP tras el checkpoint |
| ControlNet | `comfyui_inject_controlnet_py_ml` | `KSampler.positive` ← `ControlNetApply` |
| IPAdapter (style/faceid) | `comfyui_inject_ipadapter_py_ml` | `KSampler.model` ← IPAdapter (tras las LoRAs) |
| hires/upscale | `comfyui_inject_hires_fix_py_ml` | `UltimateSDUpscale` tras el `VAEDecode` |
| FaceDetailer | `comfyui_build_facedetailer_workflow_py_ml` | regenera caras del `VAEDecode` |

Orden fijo: `loras → controlnet → ipadapter → facedetailer → hires`. El IPAdapter se aplica sobre
el MODEL ya modificado por los LoRAs (orden correcto). Tras FaceDetailer el mixer deja un único
`SaveImage` (el del detailer).

### Ejemplo canónico (≥3 capacidades, juzgado)

```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from pipelines.comfyui_generate_mixed_oneshot import comfyui_generate_mixed_oneshot

# txt2img dreamshaper + 2 LoRAs + FaceDetailer (3 capacidades). Activar/desactivar = cambiar args.
res = comfyui_generate_mixed_oneshot(
    "txt2img",
    "a heroic knight portrait, 3d render style, dramatic lighting, detailed face",
    checkpoint="dreamshaper_8.safetensors",
    capabilities={
        "loras": [
            {"name": "SD15_3d_render_redmond.safetensors", "strength_model": 0.9},
            {"name": "SD15_detail_tweaker.safetensors", "strength_model": 0.5, "strength_clip": 0.5},
        ],
        "facedetailer": {"denoise": 0.45},
        # "ipadapter": {"ref_image": "face.png", "mode": "faceid"},  # se activa con solo añadirla
        # "hires": {"upscale_by": 1.5},
    },
    dest="/tmp/comfy_mixed", seed=42, judge=True,
)
print(res["ok"], res["prompt_id"], res["capabilities_active"], res["judge"])
```

### Límite conocido (8GB / piezas actuales)

- **hires + facedetailer no encadenan**: ambos toman su imagen del `VAEDecode` del render base, así
  que combinarlos deja a uno sin efecto sobre la salida final (con los dos activos, hires "gana" y
  facedetailer queda sin consumidor). Usa uno U otro por workflow. El resto de combinaciones
  (LoRAs + ControlNet + IPAdapter + uno de los dos post-procesos) encadenan limpio.
- **VRAM**: en 8GB lowvram con SD1.5 entran ~2-3 capacidades modestas (p.ej. 2 LoRAs + FaceDetailer
  a 512px). Apilar IPAdapter FaceID + ControlNet + hires + facedetailer a la vez puede dar OOM —
  baja resolución o reduce capacidades. `mixer` no valida VRAM; el OOM aflora en `wait`.
- **Incompatibilidad explícita, no silenciosa**: ControlNet sin `control_image` o IPAdapter sin
  `ref_image` lanzan `ValueError` del inyector (no petan a medias). Las imágenes de control/referencia
  deben estar en el `input/` del servidor antes de encolar.

## Fronteras

- **No genera ni descarga modelos**: una skill referencia checkpoints/LoRAs por nombre; deben
  estar ya instalados en ComfyUI (`comfyui_download_model`, otro flujo). `build_skill_workflow` es
  puro y no valida contra el servidor — usa `comfyui_validate_workflow` antes de encolar si dudas.
- **`base_workflow` solo de texto**: `txt2img`, `flux`, `sdxl_refiner`. Las bases que parten de una
  imagen (`img2img`, `inpaint`, `controlnet`) lanzan `SkillWorkflowError`; para esas, monta el
  workflow con los builders del grupo `comfyui` directamente.
- **`blocks` soportados**: `facedetailer` y `hires_fix`. Otros post-procesos (IPAdapter,
  multi-ControlNet) se añaden creando su función-inyector hermana y registrándola en el dispatcher
  de `build_skill_workflow`.
- **El juicio (`comfyui-judge`) vive en su grupo**: este grupo lo *consume* (vía
  `generate_with_skill_oneshot` con `judge=True`), pero el panel multi-juez —estético + CLIP +
  LLM-vision— se documenta en [`comfyui-judge`](comfyui-judge.md). Aquí solo se acumula su `score`
  en `score_mean` (`comfyui_update_skill_score`) y se usa como gate del bump.
- **El bump solo sube versiones, no genera ni juzga**: `comfyui_bump_skill_version` aplica el patch
  y registra la mejora; generar la imagen y puntuarla es trabajo del pipeline + el panel-juez. Una
  variante que no supera a la vigente se descarta sola (el gate la rechaza).
- **La librería es metadata local**: vive bajo `~/ComfyUI/skills_library` (no toca el venv ni los
  modelos en disco). No tiene repo propio ni se indexa — es estado vivo, como un `operations.db`.
- **Las funciones impuras del grupo** (save/load/list, ask_llm_vision) no llevan unit tests por
  diseño (I/O de disco / red); `build_skill_workflow` e `inject_hires_fix` son puras y sí tienen
  tests de estructura offline (`python/functions/ml/tests/test_comfyui_build_skill_workflow.py`,
  `test_comfyui_inject_hires_fix.py`).
