tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1184Shell 70Rust 62Markdown 501503 Definitions

recetas: el chino entero, y SIN tocar nada de GNOME

El techo que bloqueaba el chino era librsvg, y la pregunta que no me había
hecho era EN QUÉ VERSIÓN entró. Medido tag a tag: fcitx5 5.1.20 y 5.1.21 no la
piden; 5.1.22 sí. libime pide Fcitx5Utils >=5.1.20 ⇒ 5.1.21 es la única versión
que destraba el motor de pinyin sin librsvg. Y fcitx5-chinese-addons encaja en
la misma ventana: su 5.1.15 exige el marco >=5.1.22, pero su 5.1.13 pide
>=5.1.20 y libime >=1.1.14 (tenemos 1.1.17). Dos rangos que se solapan en un
punto.

Lo que esto EVITA, medido promoviendo y revirtiendo de verdad: las 7 recetas de
la raíz de introspección cambiaban 5 de sus 7 hashes y los dependientes de
GNOME los 35. La cola de GNOME no es «recetas esperando a promoverse» sino un
mundo paralelo dinámico+introspectado: su glib es default_library=both contra la
static del corpus, misma versión y otro hash, y glib-introspected es una SEGUNDA
pasada de glib que depende de la primera y de gobject-introspection, con 31
recetas de la cola colgando de ella. Esa decisión sigue viva pero ya sólo para
orca, no para el chino.

Del salto 5.1.14 → 5.1.21, los cuatro requisitos nuevos se comprobaron contra
los dos CMakeLists y no contra los makedepends de otra distro:
  · nlohmann_json ya estaba en el corpus, y REEMPLAZA a json-c, que sale
  · wayland-protocols 1.48 cubre el >=1.46
  · PlasmaWaylandProtocols se apaga: la rama else() usa su XML vendorizado de
    20 KB. Leer el else() antes de escribir una receta ahorró la tercera
  · yoga es receta nueva (commit anterior)

Las seis recetas nuevas de esta unidad:
  libime-lm/-dict/-table   los datos que libime baja por red; cada tarball
                           pineado con el sha que upstream declara en su propio
                           CMakeLists. Tres tarballs ⇒ tres recetas, porque
                           [source] admite UNA sola fuente
  fcitx5-pinyin-stroke     py_stroke.mb y py_table.mb, que pinyinhelper baja
  fcitx5-pinyin-table      SIN perilla que lo evite. El addon los busca en
                           EJECUCIÓN (stroke.cpp:47, pinyinlookup.cpp:138), así
                           que van en [deps] runtime y el CMakeLists se parchea
                           para no bajar ni reinstalar nada
  opencc                   simplificado <-> tradicional. Autocontenido: vendoriza
                           sus 4 deps con contenido real, cero submódulos, cero
                           descargas ⇒ el tradicional sale por UNA receta
  fcitx5-chinese-addons    pinyin + table + helper + chttrans

Y un arreglo de mi propio guardián de libime: pedía TRES herramientas y son
CUATRO — libime_tabledict es la que compila los ocho diccionarios por tabla. Lo
había escrito contra lo que yo pensaba usar, no contra lo que el paquete
promete. Un guardián que pide de menos no rechaza nada: deja el hueco hasta que
una receta dos pasos más abajo no encuentra su binario.

cloudpinyin queda APAGADO por decisión y no por falta de dep: manda lo que
teclea el usuario a un servidor remoto. ENABLE_GUI=Off es una pérdida real
(no hay fcitx5-qt) y queda anotada en la receta, no disimulada.

0359f7dc3d4cd45302e0a652038c4cc4e7763fed · by Sergio · (2 d ago) · parent b3bf05caa · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

9 files · +645 −31

diff --git a/recipes/fcitx5-chinese-addons.toml b/recipes/fcitx5-chinese-addons.toml
new file mode 100644
index 00000000..b957f93b
--- /dev/null
+++ b/recipes/fcitx5-chinese-addons.toml
@@ -0,0 +1,158 @@
+# fcitx5-chinese-addons 5.1.13 — el TERCER método de entrada. Con esto se escribe chino.
+#
+# ══ LA VERSIÓN ES EL RESULTADO DE UNA MEDICIÓN, NO LA ÚLTIMA ═══════════════════════════════════
+# Upstream va por **5.1.15**, y su `CMakeLists.txt` fija `REQUIRED_FCITX_VERSION 5.1.22`. Pero
+# fcitx5 ≥5.1.22 exige `librsvg-2.0`, que sólo existe en `incoming-gnome` enredado con la pila de
+# introspección de esa cola, y promoverla cuesta **35 recetas de GNOME rearmando** (medido
+# promoviendo y revirtiendo de verdad; la nota larga está en `recipes/fcitx5.toml`).
+#
+# Entonces se buscó hacia atrás, tag por tag, en vez de dar el techo por bueno:
+#
+#     addons    pide fcitx5   pide libime
+#     5.1.15      5.1.22        1.1.14
+#     5.1.14      5.1.22        1.1.14
+#     5.1.13      **5.1.20**    1.1.14      ← acá
+#     5.1.12      5.1.13        1.1.13
+#
+# Y `librsvg` entró en fcitx5 **5.1.22**, así que `5.1.21` lo cumple sin pedirla. Nuestro `libime`
+# es 1.1.17 ≥ 1.1.14. **Dos rangos que se solapan en un punto**, y ese punto da el chino sin tocar
+# ni una receta de GNOME. La lección de método: *un techo de versión se mide en los dos extremos
+# antes de aceptarlo como coste.*
+#
+# ══ ⚠ EL PARCHE DE `pinyinhelper`: DOS DESCARGAS SIN PERILLA ════════════════════════════════════
+# `modules/pinyinhelper/CMakeLists.txt` baja dos tarballs con `fcitx5_download`, y el módulo entra
+# **incondicionalmente** desde `modules/CMakeLists.txt` — no hay `option()` que lo evite. El sandbox
+# no tiene red, así que eso mata el build.
+#
+# Lo que decide la forma del parche es **cuándo** se usa el dato, y se comprobó en la fuente:
+# `stroke.cpp:47` y `pinyinlookup.cpp:138` lo buscan como `"pinyinhelper/py_stroke.mb"` por la vía
+# de `StandardPath` — o sea **en EJECUCIÓN, no al compilar**. ⇒ el addon no necesita el fichero para
+# construirse: le basta encontrarlo en la imagen.
+#
+# ⇒ Las descargas, las extracciones y los `install(FILES …)` de esos dos `.mb` se borran del
+# CMakeLists, y los datos llegan por `recipes/fcitx5-pinyin-stroke.toml` y
+# `fcitx5-pinyin-table.toml`, declaradas en `[deps] runtime`. Si en vez de eso hubiera dejado el
+# `install`, el mismo fichero viajaría en DOS artefactos a la misma ruta.
+#
+# Cada `sed` se COMPRUEBA antes: si upstream mueve una de esas líneas, el configure falla diciendo
+# cuál, en vez de volver a intentar una descarga que no puede funcionar.
+#
+# ══ LAS PERILLAS, Y QUÉ SE PIERDE CON CADA UNA ═════════════════════════════════════════════════
+#   ENABLE_OPENCC=On       simplificado ↔ tradicional. **Encendida**: `recipes/opencc.toml` es
+#                          autocontenida (vendoriza sus 4 deps) así que el tradicional sale por una
+#                          sola receta. Apagarla dejaría la mitad del chino escrito afuera.
+#   ENABLE_DATA=On         compila `chaizi.txt`, que viene EN el árbol — no baja nada. Es el
+#                          diccionario de descomposición de caracteres.
+#   ENABLE_TOOLS=On        `scel2org5`: convierte diccionarios de Sogou a texto. Pequeño, sin deps
+#                          nuevas, y es cómo alguien importa su propio vocabulario.
+#   ENABLE_GUI=Off         la ventana de configuración, que es Qt (`Qt Concurrent` +
+#                          `Fcitx5QtWidgetsAddons`). No tenemos `fcitx5-qt` en el corpus; la
+#                          configuración se edita por fichero. **Esto es una pérdida real y queda
+#                          anotada**, no disimulada: el usuario no tiene diálogo para elegir
+#                          disposición de teclado ni diccionarios.
+#   ENABLE_BROWSER=Off     necesita `QtWebEngineWidgets`, que es Chromium entero. No.
+#   ENABLE_CLOUDPINYIN=Off **decisión, no falta de dep** (sólo pedía `libcurl`, que está): el
+#                          «pinyin en la nube» manda lo que teclea el usuario a un servidor remoto
+#                          para mejorar los candidatos. En una distro que se construye entera desde
+#                          fuente para ser auditable, eso no se enciende por defecto.
+#   ENABLE_TEST=Off        su `option()` viene en On y no se corre la suite acá.
+#
+# `Boost 1.61 OPTIONAL_COMPONENTS json`: nuestro boost compila `iostreams` y no `json`. Es
+# OPCIONAL —`if (TARGET Boost::json)`— así que no falla; lo que se pierde es una ruta de importación
+# de diccionarios en formato json.
+name    = "fcitx5-chinese-addons"
+version = "5.1.13"
+license = "GPL-2.0-or-later"
+
+[source]
+tarball = "https://github.com/fcitx/fcitx5-chinese-addons/archive/refs/tags/5.1.13/fcitx5-chinese-addons-5.1.13.tar.gz"
+sha256  = "ee056e7ac31f5f9c242cee43f7ad8605e2cc188a7bbcc46f869332a49058244a"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "dynamic"
+flags    = []
+
+[build.phases]
+configure = '''
+set -e
+ZW="$PWD/.zwrap"; mkdir -p "$ZW"
+printf '#!/bin/sh\nexec /opt/zig/zig cc -mcpu=baseline "$@"\n'  > "$ZW/cc"
+printf '#!/bin/sh\nexec /opt/zig/zig c++ -mcpu=baseline "$@"\n' > "$ZW/cxx"
+chmod +x "$ZW/cc" "$ZW/cxx"
+
+# ── El parche de pinyinhelper. Se comprueba ANTES de aplicar, por fichero y por marcador.
+PH=modules/pinyinhelper/CMakeLists.txt
+[ -f "$PH" ] || { echo "fcitx5-chinese-addons: no existe $PH — upstream reorganizó el árbol" >&2; exit 1; }
+for marca in fcitx5_download fcitx5_extract; do
+  grep -q "$marca" "$PH" || { echo "fcitx5-chinese-addons: $PH ya no usa $marca — el parche de red sobra o cambió de forma; REVISAR antes de seguir" >&2; exit 1; }
+done
+# Las llamadas ocupan varias líneas (abren paréntesis y lo cierran más abajo), así que se borran por
+# RANGO desde la llamada hasta su línea de cierre `)`, no por línea suelta.
+python3 - "$PH" <<'PYEOF'
+import sys, re
+p = sys.argv[1]
+s = open(p, encoding='utf-8').read()
+# Fuera download/extract (bajan por red) y los install de los .mb (los entregan las recetas de datos)
+patrones = [
+    r'fcitx5_download\([^)]*\)\n',
+    r'fcitx5_extract\([^)]*\)\n',
+    r'install\(FILES "\$\{PY_(?:STROKE|TABLE)_TGT\}"[^)]*\)\n',
+]
+for pat in patrones:
+    s, n = re.subn(pat, '', s)
+    if n == 0:
+        sys.exit(f"fcitx5-chinese-addons: el patrón {pat!r} no coincidió con nada en {p}")
+open(p, 'w', encoding='utf-8').write(s)
+PYEOF
+# Comprobación de salida: que no quede NI UNA referencia a la red en ese fichero.
+grep -qE 'fcitx5_download|fcitx5_extract|download\.fcitx-im\.org' "$PH" && { echo "fcitx5-chinese-addons: $PH todavía referencia la red tras el parche" >&2; grep -nE 'fcitx5_download|fcitx5_extract|download' "$PH" >&2; exit 1; }
+echo "fcitx5-chinese-addons: pinyinhelper sin descargas"
+
+rm -rf build
+cmake -B build -G Ninja -Wno-dev \
+  -DCMAKE_C_COMPILER="$ZW/cc" -DCMAKE_CXX_COMPILER="$ZW/cxx" \
+  -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
+  -DCMAKE_INSTALL_LIBDIR=lib \
+  -DPKG_CONFIG_ARGN=--static \
+  -DENABLE_OPENCC=On -DENABLE_DATA=On -DENABLE_TOOLS=On \
+  -DENABLE_GUI=Off -DENABLE_BROWSER=Off -DENABLE_CLOUDPINYIN=Off \
+  -DENABLE_TEST=Off
+'''
+compile = "cmake --build build"
+# Guardián por SUBCADENA y no por nombre exacto: la convención de fcitx5 es `lib<nombre>.so`, y
+# pedir `pinyin.so` daría un falso negativo que rechaza un build bueno — pasó con `fcitx5-hangul` y
+# costó una vuelta entera.
+#
+# Se exigen los DOS métodos de entrada (`pinyin` y `table`) y sus `.conf`, que es lo que hace que el
+# daemon sepa que existen. Un addon sin su `.conf` en `share/fcitx5/addon/` está en el disco y es
+# invisible: el daemon no lo carga nunca y no hay ningún error.
+install = '''
+set -e
+DESTDIR=/out cmake --install build
+for m in pinyin table pinyinhelper chttrans punctuation fullwidth; do
+  so=$(find /out/usr/lib/fcitx5 -name "*$m*.so" 2>/dev/null | head -1)
+  [ -n "$so" ] || { echo "fcitx5-chinese-addons: no hay ningún *$m*.so en /usr/lib/fcitx5" >&2; ls -R /out/usr/lib/fcitx5 2>/dev/null | head -30 >&2; exit 1; }
+  [ -f "/out/usr/share/fcitx5/addon/$m.conf" ] || { echo "fcitx5-chinese-addons: falta share/fcitx5/addon/$m.conf — el addon existiría en disco y el daemon no lo cargaría nunca" >&2; ls /out/usr/share/fcitx5/addon 2>/dev/null >&2; exit 1; }
+done
+# `chttrans` es el que depende de opencc. Que su `.so` enlace la compartida es lo que separa «el
+# addon está» de «el addon convierte»: sin ese NEEDED, cargaría y devolvería el texto sin tocar.
+ch=$(find /out/usr/lib/fcitx5 -name '*chttrans*.so' | head -1)
+readelf -d "$ch" 2>/dev/null | grep -q 'libopencc' || { echo "fcitx5-chinese-addons: chttrans no enlaza libopencc — el tradicional NO funcionaría y no habría error" >&2; readelf -d "$ch" 2>/dev/null | grep NEEDED >&2; exit 1; }
+# El diccionario de descomposición que produce ENABLE_DATA.
+[ -s /out/usr/share/fcitx5/pinyin/chaizi.dict ] || { echo "fcitx5-chinese-addons: falta o está vacío share/fcitx5/pinyin/chaizi.dict — lo produce ENABLE_DATA con libime_pinyindict" >&2; exit 1; }
+n=$(find /out/usr/lib/fcitx5 -name '*.so' | wc -l)
+echo "fcitx5-chinese-addons: $n addon(s), chttrans enlazado a opencc, chaizi.dict presente"
+'''
+
+[deps]
+build = ["cmake", "samurai", "extra-cmake-modules", "pkgconf", "fcitx5", "libime", "boost",
+         "opencc", "zstd", "gettext-tiny", "libxkbcommon", "wayland", "wayland-protocols",
+         "glib", "pcre2", "libffi", "expat", "zlib", "dbus", "binutils"]
+# ⚠ Datos que el addon busca EN EJECUCIÓN, no al construir. Van en `runtime` precisamente por eso:
+# no entran en el hash del build —`runtime` no re-hashea— pero sí en la clausura de la imagen, que
+# es lo que hace que los ficheros estén cuando el daemon los busque. Sin esto, los addons cargan y
+# no encuentran sus tablas: ni trazos, ni pronunciación, ni conversión, ni candidatos.
+runtime = ["libime-lm", "libime-dict", "libime-table",
+           "fcitx5-pinyin-stroke", "fcitx5-pinyin-table", "opencc"]
diff --git a/recipes/fcitx5-pinyin-stroke.toml b/recipes/fcitx5-pinyin-stroke.toml
new file mode 100644
index 00000000..7e61b75e
--- /dev/null
+++ b/recipes/fcitx5-pinyin-stroke.toml
@@ -0,0 +1,60 @@
+# fcitx5-pinyin-stroke — la tabla de TRAZOS: buscar un carácter dibujando su orden de escritura.
+#
+# ══ POR QUÉ ESTA RECETA EXISTE ═════════════════════════════════════════════════════════════════
+# El addon `pinyinhelper` de `fcitx5-chinese-addons` baja DOS tarballs de datos en tiempo de build
+# (`modules/pinyinhelper/CMakeLists.txt`), y lo hace **sin ninguna perilla que lo apague**: el
+# módulo entra incondicionalmente desde `modules/CMakeLists.txt`. El sandbox de takana no tiene red,
+# así que eso mata el build.
+#
+# ⚠ **Y no alcanza con pre-colocar el `.mb`: hay que entender la macro.** `fcitx5_download` genera
+# un script que mira `if (NOT EXISTS <destino>)` antes de bajar — o sea que **sí** se salta la
+# descarga si el fichero ya está y el sha coincide (leído en
+# `Fcitx5Download.cmake.in` de nuestro propio artefacto de fcitx5, no supuesto). Pero el destino es
+# el **TARBALL** dentro del directorio de build, y una receta de takana tiene UNA sola fuente que se
+# extrae antes de que corran las fases: el tarball no queda a mano para copiarlo.
+#
+# ⇒ Entonces el reparto es: esta receta trae el dato ya extraído e instalado, y
+# `fcitx5-chinese-addons` **parchea** su `pinyinhelper/CMakeLists.txt` para no bajar nada y apuntar
+# al fichero que esta receta deja. Es la misma forma que `sonic` con espeak-ng: una dep existe para
+# que el consumidor no salga a la red.
+#
+# ══ QUÉ ES EL DATO ═════════════════════════════════════════════════════════════════════════════
+# `py_stroke.mb` asocia cada carácter con su secuencia de trazos (horizontal, vertical, gancho…).
+# Sirve para **encontrar un carácter cuya pronunciación no sabés**, que es exactamente el caso en
+# el que un método por pinyin no te sirve de nada. Es un dato tabular, no se compila: se copia.
+#
+# El sha256 es el que `fcitx5-chinese-addons` declara en su propio CMakeLists, así que está pineado
+# contra lo que el proyecto afirma y no contra lo que el servidor sirva hoy. Comprobado que coinciden.
+name    = "fcitx5-pinyin-stroke"
+version = "20250329"
+license = "GPL-2.0-or-later"
+
+[source]
+tarball = "https://download.fcitx-im.org/data/py_stroke-20250329.tar.gz"
+sha256  = "c1a7ca7225d3614ab83353fc827503006a980447762018f53760425d7b5303a6"
+# `py_stroke.mb` está en la raíz del tarball (comprobado con `tar -tf`), sin directorio contenedor.
+strip_components = 0
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+flags    = []
+
+[build.phases]
+configure = 'true'
+compile   = 'true'
+# Guardián con piso de tamaño: el dato son cientos de KB. Un `.mb` vacío o truncado deja el addon
+# cargando una tabla de trazos sin trazos — buscarías un carácter y no saldría ninguno.
+install = '''
+set -e
+[ -f py_stroke.mb ] || { echo "fcitx5-pinyin-stroke: el tarball no dejó py_stroke.mb en la raíz" >&2; ls -la >&2; exit 1; }
+n=$(stat -c%s py_stroke.mb)
+[ "$n" -gt 102400 ] || { echo "fcitx5-pinyin-stroke: py_stroke.mb son sólo $n bytes — esperaba >100 KiB" >&2; exit 1; }
+mkdir -p /out/usr/share/fcitx5/pinyinhelper
+cp py_stroke.mb /out/usr/share/fcitx5/pinyinhelper/
+echo "fcitx5-pinyin-stroke: py_stroke.mb $n B en /usr/share/fcitx5/pinyinhelper"
+'''
+
+[deps]
+build = []
diff --git a/recipes/fcitx5-pinyin-table.toml b/recipes/fcitx5-pinyin-table.toml
new file mode 100644
index 00000000..db2d3960
--- /dev/null
+++ b/recipes/fcitx5-pinyin-table.toml
@@ -0,0 +1,45 @@
+# fcitx5-pinyin-table — la tabla de PRONUNCIACIÓN: qué pinyin tiene cada carácter.
+#
+# Hermana de `fcitx5-pinyin-stroke`; el por qué de que los datos de `pinyinhelper` salgan a recetas
+# propias está explicado ahí, y es que el addon los baja por red sin perilla para evitarlo.
+#
+# `py_table.mb` es el camino inverso al del motor de entrada: dado un carácter, **decir cómo se
+# pronuncia**. Es lo que permite que el helper muestre el pinyin de lo que acabás de escribir o de
+# lo que pegaste del portapapeles — útil para alguien que está aprendiendo, y la única forma de
+# resolver un carácter que reconocés de vista y no sabés teclear.
+#
+# ⚠ **La versión es de 2012 y eso NO es un descuido**: `py_table-20121124` es lo último que publica
+# upstream, y es lo que `fcitx5-chinese-addons` 5.1.13 pinea en su CMakeLists. La pronunciación de
+# los caracteres no cambia; el dato está terminado. Si algún día aparece uno nuevo, esta receta lo
+# seguirá como siguió al de trazos.
+name    = "fcitx5-pinyin-table"
+version = "20121124"
+license = "GPL-2.0-or-later"
+
+[source]
+tarball = "https://download.fcitx-im.org/data/py_table-20121124.tar.gz"
+sha256  = "42146ac97de6c13d55f9e99ed873915f4c66739e9c11532a34556badf9792c04"
+# `py_table.mb` está en la raíz del tarball (comprobado con `tar -tf`).
+strip_components = 0
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+flags    = []
+
+[build.phases]
+configure = 'true'
+compile   = 'true'
+install = '''
+set -e
+[ -f py_table.mb ] || { echo "fcitx5-pinyin-table: el tarball no dejó py_table.mb en la raíz" >&2; ls -la >&2; exit 1; }
+n=$(stat -c%s py_table.mb)
+[ "$n" -gt 51200 ] || { echo "fcitx5-pinyin-table: py_table.mb son sólo $n bytes — esperaba >50 KiB" >&2; exit 1; }
+mkdir -p /out/usr/share/fcitx5/pinyinhelper
+cp py_table.mb /out/usr/share/fcitx5/pinyinhelper/
+echo "fcitx5-pinyin-table: py_table.mb $n B en /usr/share/fcitx5/pinyinhelper"
+'''
+
+[deps]
+build = []
diff --git a/recipes/fcitx5.toml b/recipes/fcitx5.toml
index 86ee4c2a..f223b330 100644
--- a/recipes/fcitx5.toml
+++ b/recipes/fcitx5.toml
@@ -100,35 +100,58 @@
 # declara en ningún perfil en este commit**: primero el marco construye, después un motor, y recién
 # entonces el par se declara junto. La alternativa —declarar el marco ya— es exactamente la figura
 # de `crun` sin podman y de `cups` sin filtros que este catálogo tiene documentada tres veces.
-# ⚠ **SE QUEDA EN 5.1.14 A PROPÓSITO, Y HAY UN TECHO MEDIDO — 2026-10-20.**
-# Upstream va por **5.1.23**; yo había pineado 5.1.14 sin comprobar si había algo más nuevo, que es
-# el defecto de método que esta nota documenta. Pero al subirlo apareció un techo real:
-#
-#   fcitx5 ≥ 5.1.22 exige `librsvg-2.0` (`CMakeLists:171`, REQUIRED dentro de
-#   `if (ENABLE_X11 OR ENABLE_WAYLAND)`, o sea inevitable acá). En 5.1.14 no lo pedía.
-#
-# Y `librsvg` sólo existe en `incoming-gnome`, enredado con la pila de introspección de esa cola.
-# **Promoverlo NO es gratis, medido**: la cola de GNOME tiene sus PROPIAS `pango`, `harfbuzz`,
-# `gdk-pixbuf` y `glib`, con hashes distintos de las del corpus — así que desde el corpus librsvg
-# resolvería contra otras deps, cambia de hash, y con él **35 recetas de GNOME**. (Mi primera
-# comprobación sólo miró que las deps EXISTIERAN en el corpus, no que tuvieran el MISMO hash; es la
-# diferencia entre una promoción gratis —`extra-cmake-modules`, 109 dependientes intactos— y una
-# cascada.)
-#
-# ⇒ Subir el marco cuesta una de estas dos campañas, y las dos son decisión del operador:
-#     promover librsvg + sus 4 de introspección  → 35 recetas de GNOME rearman
-#     autorar un `librsvg` de corpus             → 5 recetas duplicadas (precedente: wireplumber)
-#
-# **Lo que el techo bloquea es SÓLO el chino** (`fcitx5-chinese-addons` exige el marco ≥5.1.22). El
-# coreano y el japonés funcionan con 5.1.14 y quedan sellados, que es lo que esta versión protege.
-# `recipes/libime.toml` está escrita y espera: pide `Fcitx5Utils 5.1.20`.
+# ⚠ **SUBE A 5.1.21, Y ESA VERSIÓN EXACTA ES EL RESULTADO DE UNA MEDICIÓN — 2026-10-20.**
+# Upstream va por 5.1.23. El techo que bloqueaba el chino era `librsvg-2.0` (REQUIRED dentro de
+# `if (ENABLE_X11 OR ENABLE_WAYLAND)`, o sea inevitable acá), y la pregunta que no me había hecho
+# era **en qué versión entró**. Medido tag a tag:
+#
+#     fcitx5    librsvg   yoga   nlohmann
+#     5.1.20      no       sí      sí
+#     5.1.21      no       sí      sí      ← acá
+#     5.1.22      SÍ       sí      sí
+#     5.1.23      SÍ       sí      sí
+#
+# `libime` pide `Fcitx5Utils ≥5.1.20` y `librsvg` entró en **5.1.22** ⇒ **5.1.21 es la única versión
+# que destraba el motor de pinyin sin tocar nada de GNOME.** Y `fcitx5-chinese-addons` encaja en la
+# misma ventana: su 5.1.15 exige el marco ≥5.1.22, pero su **5.1.13** pide ≥5.1.20 (y libime
+# ≥1.1.14, que nuestro 1.1.17 cumple). Dos rangos que se solapan en un punto, y ese punto es éste.
+#
+# ⚠ **LA RUTA POR GNOME QUE ESTO EVITA, CON SU COSTE MEDIDO** — promoví y revertí de verdad, no lo
+# estimé: las 7 recetas de la raíz de introspección cambiaban **5 de sus 7 hashes** y los
+# dependientes de GNOME **los 35**. La causa no es «faltaba promover unas recetas»: la cola de GNOME
+# es un **mundo paralelo dinámico+introspectado**. Su `glib` es `default_library=both` contra la
+# `static` del corpus —misma versión 2.88.1, otro hash—, y `glib-introspected` es una **SEGUNDA
+# pasada** de glib que depende de la primera Y de `gobject-introspection`, con **31 recetas de la
+# cola colgando de ella**. Promover esa raíz la haría construir contra la glib ESTÁTICA del corpus.
+# Esa decisión sigue viva, pero **ya sólo para `orca`** (`at-spi2-core` + `py3-gobject`), no para el
+# chino.
+#
+# ══ LOS CUATRO REQUISITOS NUEVOS DEL SALTO 5.1.14 → 5.1.21 ═════════════════════════════════════
+# Comparando los `find_package`/`pkg_check_modules` de los dos CMakeLists, no de los makedepends de
+# otra distro — que es el error que cometí inventando una dep `fmt` para esta misma receta:
+#   · `nlohmann_json`                ya en el corpus (3.12.0) — y **reemplaza a `JsonC`**, que sale
+#                                    de las deps porque 5.1.21 ya no lo busca
+#   · `WaylandProtocols ≥1.46`       ya cubierto (tenemos 1.48)
+#   · `PlasmaWaylandProtocols ≥1.20` **se apaga**. Sólo existe en `incoming-kde`, pero no hace falta
+#                                    traerlo: con `USE_SYSTEM_PLASMA_WAYLAND_PROTOCOLS=Off` la rama
+#                                    `else()` usa su XML **vendorizado en el árbol** (20 KB,
+#                                    comprobado presente en el tarball). Leer el `else()` antes de
+#                                    escribir una receta: tercera vez que esa costumbre ahorra una.
+#   · `yoga`                         **receta nueva** — es un SUBMÓDULO git y el tarball lo trae
+#                                    vacío (el único directorio vacío del tarball), así que
+#                                    `USE_SYSTEM_YOGA=On` no es una preferencia: sin él
+#                                    `add_subdirectory(third_party/yoga/yoga)` falla al CONFIGURAR.
+#
+# Lo que este salto arrastra: `fcitx5-hangul` y `fcitx5-anthy` rearman, porque dependen del marco.
+# Son dos recetas del corpus y el coreano y el japonés ya estaban sellados — se reconstruyen y se
+# vuelve a comprobar que sigan dando su addon.
 name    = "fcitx5"
-version = "5.1.14"
+version = "5.1.21"
 license = "LGPL-2.1-or-later"
 
 [source]
-tarball = "https://github.com/fcitx/fcitx5/archive/refs/tags/5.1.14/fcitx5-5.1.14.tar.gz"
-sha256  = "c81bbf0156d083e4c7e7817e658122c33a8c1e585962467f251bc9614bdfee47"
+tarball = "https://github.com/fcitx/fcitx5/archive/refs/tags/5.1.21/fcitx5-5.1.21.tar.gz"
+sha256  = "8211fe5996db22254e5df9617cbd45873ae7fab82e7e0c42bde5a197299d1276"
 
 [build]
 compiler = "zig-cc"
@@ -162,6 +185,7 @@ cmake -B build -G Ninja -Wno-dev \
   -DENABLE_X11=Off -DENABLE_ENCHANT=Off -DUSE_SYSTEMD=Off \
   -DENABLE_TEST=Off -DENABLE_TESTING_ADDONS=On -DENABLE_DOC=Off \
   -DBUILD_SPELL_DICT=Off -DENABLE_LIBUUID=Off \
+  -DUSE_SYSTEM_YOGA=On -DUSE_SYSTEM_PLASMA_WAYLAND_PROTOCOLS=Off \
   -DPKG_CONFIG_ARGN=--static
 '''
 compile = "cmake --build build"
@@ -220,9 +244,13 @@ echo "fcitx5: daemon + $n addon(s) de fábrica"
 # los doce módulos que este build pide: `cairo`, `gio-2.0`, `gio-unix-2.0`, `pango`, `pangocairo`,
 # `gdk-pixbuf-2.0`, `wayland-client`, `wayland-cursor`, `xkbcommon`, `dbus-1`, `libuv`, `json-c`.
 # Los doce resuelven, cero fallan.
+#
+# ⚠ Esa lista de doce es de la **5.1.14** y se deja como el registro de lo que se midió entonces. En
+# **5.1.21 son once**: `json-c` desaparece, reemplazado por `nlohmann_json`, que es *headers-only* y
+# se encuentra con `find_package` y no con pkg-config — o sea que no entra en esa comprobación.
 build = ["cmake", "samurai", "extra-cmake-modules", "pkgconf", "iso-codes", "xkeyboard-config",
          "libxkbcommon", "wayland", "wayland-protocols", "cairo", "pango", "gdk-pixbuf",
          "glib", "libpng", "fontconfig", "freetype", "pixman", "fribidi", "harfbuzz",
          "pcre2", "libffi",
          "dbus", "libuv", "nlohmann-json", "expat", "zlib", "gettext-tiny", "util-linux",
-         "libxml2", "json-c", "python3"]
+         "libxml2", "yoga", "python3"]
diff --git a/recipes/libime-dict.toml b/recipes/libime-dict.toml
new file mode 100644
index 00000000..fec96eee
--- /dev/null
+++ b/recipes/libime-dict.toml
@@ -0,0 +1,67 @@
+# libime-dict — el DICCIONARIO de pinyin. Es lo que convierte la pronunciación en caracteres.
+#
+# Hermana de `libime-lm`: el `data/CMakeLists.txt` de libime baja tres tarballs en tiempo de build y
+# el sandbox de takana no tiene red, así que los datos salen a recetas con su sha pineado. El
+# `[source]` admite una sola fuente ⇒ un tarball, una receta. La nota larga está en `libime-lm.toml`.
+#
+# ══ QUÉ TRAE Y QUÉ SALE ════════════════════════════════════════════════════════════════════════
+# El tarball deja DOS ficheros de texto en la raíz, y los dos se compilan con la misma herramienta:
+#
+#     dict_sc.txt    → sc.dict     el diccionario del chino simplificado, el que se usa a diario
+#     dict_extb.txt  → extb.dict   la Extensión B de Unicode: caracteres raros e históricos
+#
+#     libime_pinyindict <entrada.txt> <salida.dict>
+#
+# Los dos se instalan, que es lo que hace upstream: `extb.dict` es opcional en el uso y el motor lo
+# carga sólo si hace falta, pero dejarlo afuera significa que un carácter poco común simplemente no
+# aparece entre los candidatos — un fallo silencioso y rarísimo de diagnosticar.
+#
+# `LIBIME_INSTALL_PKGDATADIR` = `${CMAKE_INSTALL_FULL_DATADIR}/libime` ⇒ `/usr/share/libime/`, y acá
+# **no se renombran** (a diferencia del modelo de lenguaje, que viaja como `zh_CN.lm`): el motor los
+# busca por estos nombres exactos.
+name    = "libime-dict"
+version = "20260907"
+license = "LGPL-2.1-or-later"
+
+[source]
+tarball = "https://download.fcitx-im.org/data/dict-20260907.tar.zst"
+sha256  = "fb75a179065e690dfc4559ce1807cbaf4fbe4f0111a5005615be9f435e6b9d76"
+# Los dos `.txt` están en la raíz del tarball (comprobado con `tar -tf`), sin directorio contenedor.
+strip_components = 0
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+flags    = []
+
+[build.phases]
+configure = '''
+set -e
+for f in dict_sc.txt dict_extb.txt; do
+  [ -f "$f" ] || { echo "libime-dict: el tarball no dejó $f en la raíz — ¿cambió strip_components?" >&2; ls -la >&2; exit 1; }
+done
+echo "libime-dict: dict_sc.txt $(stat -c%s dict_sc.txt) B · dict_extb.txt $(stat -c%s dict_extb.txt) B"
+'''
+compile = '''
+set -e
+libime_pinyindict dict_sc.txt   sc.dict
+libime_pinyindict dict_extb.txt extb.dict
+'''
+# Guardián con piso de tamaño, por lo de siempre: un `.dict` de cero bytes se sella igual y deja al
+# motor cargando un diccionario sin palabras — teclearías pinyin y no saldría ningún candidato.
+install = '''
+set -e
+mkdir -p /out/usr/share/libime
+cp sc.dict extb.dict /out/usr/share/libime/
+for f in sc.dict extb.dict; do
+  t=/out/usr/share/libime/$f
+  [ -s "$t" ] || { echo "libime-dict: $f quedó vacío o ausente — NO sello" >&2; exit 1; }
+  n=$(stat -c%s "$t")
+  [ "$n" -gt 102400 ] || { echo "libime-dict: $f son sólo $n bytes — esperaba >100 KiB; no se escribió" >&2; exit 1; }
+done
+echo "libime-dict: sc.dict $(stat -c%s /out/usr/share/libime/sc.dict) B + extb.dict $(stat -c%s /out/usr/share/libime/extb.dict) B"
+'''
+
+[deps]
+build = ["libime"]
diff --git a/recipes/libime-lm.toml b/recipes/libime-lm.toml
new file mode 100644
index 00000000..0e4254e0
--- /dev/null
+++ b/recipes/libime-lm.toml
@@ -0,0 +1,86 @@
+# libime-lm — el MODELO DE LENGUAJE del chino. Sin esto, el pinyin no convierte.
+#
+# ══ POR QUÉ ES UNA RECETA APARTE Y NO UN `ENABLE_DATA=On` ══════════════════════════════════════
+# El `data/CMakeLists.txt` de libime hace **tres descargas en tiempo de build** desde el servidor de
+# fcitx, y el sandbox de takana no tiene red a propósito: una fuente que se baja al construir no es
+# reproducible ni auditable. Por eso `recipes/libime.toml` va con `ENABLE_DATA=Off` y los datos
+# salen a recetas hermanas — ésta, `libime-dict` y `libime-table` — con su tarball pineado.
+#
+# Y el `[source]` de una receta admite **UNA sola** fuente (`crates/takana-core/src/recipe.rs`:
+# repo+commit, tarball+sha256 o dir, excluyentes). Tres tarballs ⇒ tres recetas. No es una
+# limitación molesta: cada dato se sella por separado y se puede rehacer sin tocar los otros.
+#
+# **El sha256 lo declara upstream en su propio CMakeLists**, así que no hay que confiar en el
+# servidor para pinearlo — se compara contra lo que el proyecto afirma. Comprobado: el fichero que
+# baja hoy de `download.fcitx-im.org` tiene exactamente ese sha.
+#
+# ══ QUÉ HACE: COMPILAR, NO COPIAR ══════════════════════════════════════════════════════════════
+# El tarball trae `lm_sc.arpa` — un modelo n-grama en texto, 77 MB comprimidos. De ahí salen DOS
+# binarios con las herramientas de `libime`, y los comandos son los de upstream, literales:
+#
+#     libime_slm_build_binary -s -a 22 -q 4 trie  lm_sc.arpa  sc.lm
+#     libime_prediction  sc.lm  lm_sc.arpa  sc.lm.predict
+#
+# Las banderas no se tocan: `-a 22` son los bits de compresión de punteros y `-q 4` los de
+# cuantización del trie de kenlm, o sea que **cambian el tamaño y la precisión del modelo**. Copiar
+# los valores de upstream es la única forma de que esto rinda como rinde en cualquier otra distro.
+#
+# ⇒ Por eso `libime` necesita `ENABLE_TOOLS=On`: sin esos dos ejecutables esta receta no tiene con
+# qué trabajar. Su guardián los exige por nombre, precisamente para que el fallo salga allá y no acá.
+#
+# ══ DÓNDE CAEN, Y POR QUÉ CON OTRO NOMBRE ══════════════════════════════════════════════════════
+# `LIBIME_INSTALL_LIBDATADIR` = `${CMAKE_INSTALL_FULL_LIBDIR}/libime` ⇒ `/usr/lib/libime/`, y
+# upstream los instala **renombrados** a `zh_CN.lm` y `zh_CN.lm.predict`: el motor los busca por
+# locale, no por el nombre del fichero de origen. Instalarlos como `sc.lm` daría un artefacto con
+# contenido que el motor no encuentra — sellado, lleno y mudo.
+name    = "libime-lm"
+version = "20260629"
+license = "LGPL-2.1-or-later"
+
+[source]
+tarball = "https://download.fcitx-im.org/data/lm_sc.arpa-20260629.tar.zst"
+sha256  = "06808333b9173e5374cf2cb5afc12d08f5625bf9abb536489cac376fc05f2e7f"
+# El tarball deja `lm_sc.arpa` en la RAÍZ, sin directorio contenedor (comprobado con `tar -tf`), así
+# que no hay ningún componente que recortar. El default de takana es 1, pensado para los releases
+# GNU; dejarlo recortaría el único fichero que hay.
+strip_components = 0
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+flags    = []
+
+[build.phases]
+configure = '''
+set -e
+[ -f lm_sc.arpa ] || { echo "libime-lm: el tarball no dejó lm_sc.arpa en la raíz — ¿cambió strip_components?" >&2; ls -la >&2; exit 1; }
+echo "libime-lm: lm_sc.arpa = $(stat -c%s lm_sc.arpa) bytes"
+'''
+# Las banderas son las de upstream, literales. `-s` deja fuera los avisos de frontera de frase.
+compile = '''
+set -e
+libime_slm_build_binary -s -a 22 -q 4 trie lm_sc.arpa sc.lm
+libime_prediction sc.lm lm_sc.arpa sc.lm.predict
+'''
+# Guardián: los dos ficheros, con un piso de tamaño. Un `.lm` de cero bytes es la figura del
+# artefacto VACÍO que este repo tiene documentada — se sella, es un cache-hit, y el motor carga sin
+# convertir nada. El piso no es un número inventado: el `.arpa` de entrada son decenas de MB y el
+# trie cuantizado de kenlm no baja de unos pocos; 1 MB es holgado y detecta el caso real (fichero
+# creado y no escrito).
+install = '''
+set -e
+mkdir -p /out/usr/lib/libime
+cp sc.lm         /out/usr/lib/libime/zh_CN.lm
+cp sc.lm.predict /out/usr/lib/libime/zh_CN.lm.predict
+for f in zh_CN.lm zh_CN.lm.predict; do
+  t=/out/usr/lib/libime/$f
+  [ -s "$t" ] || { echo "libime-lm: $f quedó vacío o ausente — NO sello un modelo que no convierte" >&2; exit 1; }
+  n=$(stat -c%s "$t")
+  [ "$n" -gt 1048576 ] || { echo "libime-lm: $f son sólo $n bytes — esperaba >1 MiB; el modelo no se escribió" >&2; exit 1; }
+done
+echo "libime-lm: zh_CN.lm $(stat -c%s /out/usr/lib/libime/zh_CN.lm) B + zh_CN.lm.predict $(stat -c%s /out/usr/lib/libime/zh_CN.lm.predict) B"
+'''
+
+[deps]
+build = ["libime"]
diff --git a/recipes/libime-table.toml b/recipes/libime-table.toml
new file mode 100644
index 00000000..2c05e569
--- /dev/null
+++ b/recipes/libime-table.toml
@@ -0,0 +1,78 @@
+# libime-table — los diccionarios de los métodos de entrada POR TABLA (wubi, cangjie, zhengma…).
+#
+# Hermana de `libime-lm` y `libime-dict`; la nota larga del por qué está en `libime-lm.toml`.
+#
+# ══ QUÉ SON, Y POR QUÉ NO SE SALTAN ════════════════════════════════════════════════════════════
+# El pinyin se teclea por pronunciación; los métodos por TABLA se teclean por **forma** del
+# carácter, y son lo que usa mucha gente que escribe rápido o que escribe cantonés. Son ocho
+# tablas, y cada `.txt` se compila a un `.main.dict` con la misma herramienta:
+#
+#     libime_tabledict <tabla.txt> <tabla.main.dict>
+#
+#     db.txt  erbi.txt  qxm.txt  wanfeng.txt  wbpy.txt  wbx.txt  zrm.txt  cj.txt
+#
+# ⚠ **Si esta receta no existiera, `fcitx5-chinese-addons` publicaría igual su método por tabla y no
+# tendría ni una tabla que cargar** — la figura de «sellado, con contenido, reproducible y sin la
+# función que promete» que este catálogo persigue. Por eso entra junto con las otras dos y no
+# después.
+#
+# ⚠ **`libime_tabledict` es una CUARTA herramienta, y mi guardián de `libime` sólo exigía tres.**
+# Había escrito el control contra las herramientas que necesitaban `libime-lm` y `libime-dict`
+# (`libime_slm_build_binary`, `libime_prediction`, `libime_pinyindict`) sin mirar el `data/` completo
+# de upstream, que usa también `LibIME::tabledict`. Es la misma clase de defecto que los cuatro
+# guardianes demasiado estrictos del 20-oct, con el signo contrario: **un guardián escrito contra lo
+# que yo iba a usar, no contra lo que el paquete promete.** Arreglado allá.
+#
+# Los `.main.dict` caen en `/usr/share/libime/` (`LIBIME_INSTALL_PKGDATADIR`), con el nombre que sale
+# de sustituir `.txt` por `.main.dict`, que es lo que hace el `foreach` de upstream.
+name    = "libime-table"
+version = "20240108"
+license = "LGPL-2.1-or-later"
+
+[source]
+tarball = "https://download.fcitx-im.org/data/table-20240108.tar.zst"
+sha256  = "3e9d87b04a393f131723472c8eaa860dd23c378a3d4f6a9005513b2a95b3614b"
+# Los ocho `.txt` están en la raíz del tarball (comprobado con `tar -tf`).
+strip_components = 0
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+flags    = []
+
+[build.phases]
+# La lista es EXPLÍCITA y no un glob, por el mismo motivo que upstream la escribe a mano: si el
+# tarball trajera un `.txt` más, un glob lo compilaría en silencio y el artefacto cambiaría de
+# contenido sin que nadie lo decidiera; y si trajera uno menos, el glob no se queja.
+configure = '''
+set -e
+falta=""
+for f in db erbi qxm wanfeng wbpy wbx zrm cj; do
+  [ -f "$f.txt" ] || falta="$falta $f.txt"
+done
+[ -z "$falta" ] || { echo "libime-table: el tarball no trae:$falta — upstream cambió el juego de tablas" >&2; ls -la >&2; exit 1; }
+echo "libime-table: las 8 tablas presentes"
+'''
+compile = '''
+set -e
+for f in db erbi qxm wanfeng wbpy wbx zrm cj; do
+  libime_tabledict "$f.txt" "$f.main.dict"
+done
+'''
+install = '''
+set -e
+mkdir -p /out/usr/share/libime
+n=0
+for f in db erbi qxm wanfeng wbpy wbx zrm cj; do
+  cp "$f.main.dict" /out/usr/share/libime/
+  t=/out/usr/share/libime/$f.main.dict
+  [ -s "$t" ] || { echo "libime-table: $f.main.dict quedó vacío — ese método por tabla no tendría nada que cargar" >&2; exit 1; }
+  n=$((n+1))
+done
+[ "$n" -eq 8 ] || { echo "libime-table: salieron $n de 8 tablas" >&2; exit 1; }
+echo "libime-table: $n tabla(s) compiladas en /usr/share/libime"
+'''
+
+[deps]
+build = ["libime"]
diff --git a/recipes/libime.toml b/recipes/libime.toml
index ca21179c..37fdfd10 100644
--- a/recipes/libime.toml
+++ b/recipes/libime.toml
@@ -16,9 +16,17 @@
 # nos da los sha en ese mismo CMakeLists, así que no hay que confiar en nadie para pinearlos.
 #
 # ⚠ **Por eso `ENABLE_TOOLS=On` es obligatorio acá y no adorno.** Las recetas de datos necesitan
-# los ejecutables de ESTA para convertir el `.arpa` y el `.txt` en los binarios que el motor lee:
-# `libime_slm_build_binary` (el modelo), `libime_prediction` y `libime_pinyindict`. Si esta receta
-# se construyera sin herramientas, las otras dos no tendrían con qué trabajar.
+# los ejecutables de ESTA para convertir el `.arpa` y los `.txt` en los binarios que el motor lee.
+# Si esta receta se construyera sin herramientas, las otras no tendrían con qué trabajar.
+#
+# ⚠ **SON CUATRO, Y MI GUARDIÁN PEDÍA TRES — corregido 2026-10-20.** Había escrito el control contra
+# las herramientas que iban a usar `libime-lm` y `libime-dict` (`libime_slm_build_binary`,
+# `libime_prediction`, `libime_pinyindict`) sin leer el `data/CMakeLists.txt` completo de upstream,
+# que usa también **`LibIME::tabledict`** para los ocho diccionarios de los métodos por TABLA. Es la
+# misma clase de defecto que los cuatro guardianes demasiado estrictos del mismo día, con el signo
+# invertido: **escribí el guardián contra lo que yo pensaba usar, no contra lo que el paquete
+# promete.** Un guardián que pide menos de lo que hace falta no rechaza nada — deja pasar el hueco
+# hasta que una receta de datos dos pasos más abajo no encuentra su binario.
 #
 # ⇒ Y hasta que esas dos existan, **libime NO se declara en ningún perfil**: un motor de conversión
 # sin modelo de lenguaje carga y no convierte nada. Es la figura de `crun` sin podman, la misma por
@@ -71,7 +79,7 @@ DESTDIR=/out cmake --install build
 n=$(find /out/usr/lib -name 'libIME*.so*' 2>/dev/null | wc -l)
 [ "$n" -gt 0 ] || { echo "libime: no quedó ninguna libIME*.so — NO sello" >&2; ls -R /out/usr/lib 2>/dev/null | head -20 >&2; exit 1; }
 falta=""
-for t in libime_slm_build_binary libime_prediction libime_pinyindict; do
+for t in libime_slm_build_binary libime_prediction libime_pinyindict libime_tabledict; do
   [ -x "/out/usr/bin/$t" ] || falta="$falta $t"
 done
 if [ -n "$falta" ]; then
@@ -79,7 +87,7 @@ if [ -n "$falta" ]; then
   ls /out/usr/bin 2>/dev/null >&2
   exit 1
 fi
-echo "libime: $n biblioteca(s) + las 3 herramientas de datos"
+echo "libime: $n biblioteca(s) + las 4 herramientas de datos"
 '''
 
 [deps]
diff --git a/recipes/opencc.toml b/recipes/opencc.toml
new file mode 100644
index 00000000..8d29928a
--- /dev/null
+++ b/recipes/opencc.toml
@@ -0,0 +1,84 @@
+# OpenCC 1.4.2 — conversión entre chino SIMPLIFICADO y TRADICIONAL.
+#
+# ══ POR QUÉ ENTRA ══════════════════════════════════════════════════════════════════════════════
+# Es la dep de `chttrans`, el addon de `fcitx5-chinese-addons` que convierte al vuelo lo que
+# escribís. Sin él, `ENABLE_OPENCC=Off` y la distro sabe escribir chino **simplificado y nada más**:
+# quien escribe en Taiwán, Hong Kong o Macao teclea el mismo pinyin y obtiene los caracteres
+# equivocados. No es un adorno regional — es la mitad de los usuarios de chino escrito.
+#
+# ⚠ Y no es «un idioma más con otra fuente»: la conversión no es carácter a carácter. Hay
+# correspondencias de uno a varios que dependen del SENTIDO de la palabra (el clásico 发 → 髮 pelo o
+# 發 emitir), y por eso OpenCC lleva diccionarios de frases y no una tabla. Eso es lo que se compila
+# en su fase `data`.
+#
+# ══ ES AUTOCONTENIDO, Y ESO SE COMPROBÓ ANTES DE ESCRIBIR LA RECETA ════════════════════════════
+# OpenCC vendoriza TODAS sus deps dentro del árbol, con contenido real y sin un solo submódulo
+# (medido sobre el tarball: `darts-clone` 4 ficheros, `marisa` 111, `rapidjson` 39, `tclap` 29; cero
+# directorios vacíos; cero `FetchContent`/`file(DOWNLOAD)`). Sus perillas `USE_SYSTEM_*` vienen
+# todas en `OFF`, así que por defecto usa lo vendorizado.
+#
+# ⇒ **Cero deps nuevas en el corpus.** Eso es lo que hace que valga la pena: el tradicional sale por
+# una sola receta. Lo contrario de `librsvg`, que arrastraba la pila de introspección de GNOME.
+#
+# `BUILD_SHARED_LIBS=ON`: `chttrans` es un addon `.so` que el daemon abre con `dlopen`, y enlazarlo
+# contra una `libopencc.a` no-PIC es el muro del 19-oct. Con `ON`, `marisa` y compañía se compilan
+# PIC como objetos de la compartida.
+#
+# `ENABLE_GTEST=OFF` / `ENABLE_BENCHMARK=OFF`: los dos están ya en `OFF` por defecto, pero se pasan
+# explícitos porque los árboles vendorizados de googletest y google-benchmark SÍ están presentes —
+# o sea que un cambio de default los construiría de verdad, y serían minutos de compilación que no
+# van a ninguna parte.
+name    = "opencc"
+version = "1.4.2"
+license = "Apache-2.0"
+
+[source]
+tarball = "https://github.com/BYVoid/OpenCC/archive/refs/tags/ver.1.4.2/opencc-1.4.2.tar.gz"
+sha256  = "8e5f5cf7fe195bd9b9be851adc9738c1ef7dc5c24441dd5878a56db4087a9a70"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "dynamic"
+flags    = []
+
+[build.phases]
+configure = '''
+ZW="$PWD/.zwrap"; mkdir -p "$ZW"
+# Envoltorio que reescribe `--target`: patrón canónico del repo para builds que inventan su triple.
+printf '%s\n' '#!/bin/sh' 'for a do' 'case "$a" in --target=*) a=--target=x86_64-linux-musl ;; esac' 'set -- "$@" "$a"' 'shift' 'done' 'exec /opt/zig/zig cc -mcpu=baseline "$@"' > "$ZW/cc"
+printf '%s\n' '#!/bin/sh' 'for a do' 'case "$a" in --target=*) a=--target=x86_64-linux-musl ;; esac' 'set -- "$@" "$a"' 'shift' 'done' 'exec /opt/zig/zig c++ -mcpu=baseline "$@"' > "$ZW/cxx"
+chmod +x "$ZW/cc" "$ZW/cxx"
+# `rm -rf build`: un `build/` fallido sobrevive en el árbol de fuentes reutilizado y el caché de
+# cmake hace que el reintento repita el error viejo en dos segundos. Deuda latente de TODAS las
+# recetas cmake del repo; acá se paga.
+rm -rf build
+cmake -B build -G Ninja -Wno-dev \
+  -DCMAKE_C_COMPILER="$ZW/cc" -DCMAKE_CXX_COMPILER="$ZW/cxx" \
+  -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
+  -DCMAKE_INSTALL_LIBDIR=lib \
+  -DBUILD_SHARED_LIBS=ON -DOPENCC_ENABLE_INSTALL=ON \
+  -DENABLE_GTEST=OFF -DENABLE_BENCHMARK=OFF \
+  -DBUILD_DOCUMENTATION=OFF -DBUILD_PYTHON=OFF
+'''
+compile = "cmake --build build"
+# Guardián: la `.so`, el `.pc` que busca el consumidor, y los DICCIONARIOS. El tercero es el que
+# importa de verdad: `find_package(OpenCC 1.0.1 REQUIRED)` pasa con la biblioteca sola, y una
+# OpenCC sin sus `.ocd2` **carga y no convierte nada** — simplificado entraría y simplificado
+# saldría, sin error. Es la figura del artefacto que promete una función y no la tiene.
+install = '''
+set -e
+DESTDIR=/out cmake --install build
+so=$(find /out/usr/lib -name 'libopencc.so*' -type f 2>/dev/null | head -1)
+[ -n "$so" ] || { echo "opencc: no quedó libopencc.so — chttrans es un .so y necesita la compartida" >&2; find /out -name 'libopencc*' >&2; exit 1; }
+pc=$(find /out/usr/lib -name 'opencc.pc' 2>/dev/null | head -1)
+[ -n "$pc" ] || echo "opencc: aviso — no hay opencc.pc; el consumidor usa find_package, así que no es fatal" >&2
+d=$(find /out/usr/share/opencc -name '*.ocd2' 2>/dev/null | wc -l)
+[ "$d" -gt 5 ] || { echo "opencc: sólo $d diccionario(s) .ocd2 — esperaba más de 5; sin ellos convierte la identidad en silencio" >&2; find /out/usr/share -maxdepth 3 2>/dev/null | head -20 >&2; exit 1; }
+c=$(find /out/usr/share/opencc -name '*.json' 2>/dev/null | wc -l)
+[ "$c" -gt 0 ] || { echo "opencc: no hay ninguna configuración .json (s2t, t2s…) — la API se nombra por config, no por diccionario" >&2; exit 1; }
+echo "opencc: $(basename "$so") + $d diccionario(s) + $c configuración(es)"
+'''
+
+[deps]
+build = ["cmake", "samurai", "pkgconf"]