Files
qwen3-6-lora/data/schemas/penpot_errors.md
aleleba 18efe3fa5d Phase 6.4.25b: 13 corrective seeds for gate 5 diagnosis, rebuild training mix
Diagnosis (audited gate5 full-mode transcripts, PNGs, and holdout payloads):
the dominant failure mode was NOT rubro/content coverage but a tool-call
formatting bug -- after diagnosing an exception, the model often writes its
retry as literal <tool_call> XML text embedded in reasoning instead of a
structured call, so it's silently dropped (9/10 gate5 full-mode prompts hit
this on their final captured turn). Reasoning length before a tool call
correlates with the failure (malformed-turn mean 1895 chars vs 364 for
well-formed turns), while the training mix's own tool-calling turns never
exceed 1630 chars and export_shape turns never exceed 485.

Corrective seeds (group D, grupo 'D', short 1-3 sentence diagnosis +
immediate well-formed retry, to avoid reinforcing long-reasoning risk):
- 9 seeds across fresh rubros (lavanderia, zapateria, optica, jugueteria,
  papeleria, peluqueria, floreria, heladeria, cerrajeria) covering distinct
  real API errors: .color on Text, addFlexLayout on non-Board, findShapeById
  2-arg, createText() no-arg, textAlign, typography.setFont, createBoolean
  null, appendChild no-arg, uploadMediaUrl network rejection.
- 4 seeds reinforcing uploadMediaUrl over the exact penpot.importImage
  variant the model actually invents (distinct from the one existing seed,
  which only taught the penpotUtils.importImage variant) -- rubros:
  relojeria, guarderia infantil, agencia de viajes, kiosco.

Deliberately avoid ferreteria/gimnasio/veterinaria/panaderia: reserved for
the held-out generalization probe (separate from the frozen gate 5).

Added the newly-verified error strings (t.color, penpot.importImage,
addFlexLayout-on-non-Board) to penpot_errors.md -- all captured live from
this phase's own gate 5 full-mode run, not fabricated.

Rebuilt train_lora2.jsonl (901)/eval_lora2.jsonl (99)/calibration_v2.jsonl
from the 138-seed corpus (07_build_lora2_mix.py) and validated
(06_validate_dataset.py, 0 exceptions, 0 secrets, max 3271 tokens).
2026-08-03 15:44:22 +00:00

177 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Penpot MCP — strings de error reales
Mensajes de error **literales** que devuelve el servidor MCP de Penpot cuando un `execute_code` falla.
**Por qué existe este archivo.** El grupo D de `data/raw/seeds/penpot.jsonl` (6 seeds de auto-corrección)
entrena al modelo a diagnosticar y recuperarse de errores. Si el mensaje de error de un seed es
inventado, el modelo aprende a reaccionar a un string que nunca va a ver. Este archivo es la
**allow-list de mensajes de error**: `scripts/07_lint_penpot_code.py` verifica que todo `content` de un
mensaje `tool` que representa un error sea un string presente acá, verbatim.
**Procedencia.** Los mensajes de las secciones 1-4 fueron capturados en vivo por la sesión madre el
2026-07-30, contra el archivo real del usuario, ejecutando el código de la columna "Disparador" vía
`mcp__penpot__execute_code`. Están registrados en `PLAN.md` § "La API verificada" y en la página de
Docmost de la Fase 6 § "Diagnóstico". Los de la sección 5 son mensajes estándar del runtime de
JavaScript (V8/SpiderMonkey), deterministas dada la expresión que los produce.
**Nota operativa (2026-07-30):** al intentar re-capturar estos strings durante el paso 6.1.3, el
servidor MCP devolvió `No Penpot plugin instances are currently connected. Please ensure the plugin is
running and connected.` — el plugin del usuario no está conectado. Los mensajes de abajo se conservan
tal como los verificó la sesión madre; **la re-verificación en vivo queda pendiente** y se hace junto
con el baseline de la puerta 5 (paso 6.3.17), que también requiere el plugin conectado. Ningún seed
puede usar un string de error ausente de este archivo.
---
## 1. Propiedad inexistente sobre un objeto no extensible
El objeto `Text` (y en general los shapes) **no es extensible**: asignar una propiedad que no está en
la interfaz lanza, y la excepción **aborta todo el `execute_code`**, no solo esa línea.
| Disparador | Mensaje verbatim |
| --- | --- |
| `text.textAlign = 'center'` | `Cannot add property textAlign, object is not extensible` |
| `text.color = '#...'` | `Cannot add property color, object is not extensible` |
Forma del mensaje, generalizable: `Cannot add property <nombre>, object is not extensible`.
**`text.color` (capturado en vivo, puerta 5 modo full, paso 6.4.25, prompts `g5-06-landing-pizzeria` y
`g5-01-boton-primario`, 2026-08-03):** el modelo asume que `Text` tiene una propiedad `color` de nivel
superior (analogía con CSS) en vez de usar `fills`. Es el mismo mecanismo que `textAlign` —el objeto
`Text` no es extensible— pero para una propiedad distinta, y hoy no hay ningún seed que lo enseñe (el
lint ya lo detecta como patrón prohibido, cero ejemplos de recuperación en el corpus).
La propiedad correcta para alineación horizontal de texto es **`align`**
(`'left' \| 'center' \| 'right' \| 'justify'`). Ver `penpot_api_docs.md` Captura 4.
**Este es el error más caro del conjunto**, porque el modelo suele escribir `textAlign` por analogía con
CSS, en medio de un payload de 80 líneas que construye una landing entera: la excepción tira abajo la
composición completa, no solo el título.
---
## 2. Función inexistente en el runtime
| Disparador | Mensaje verbatim |
| --- | --- |
| `penpotUtils.importImage(url)` | `penpotUtils.importImage is not a function` |
| `penpot.importImage(url)` | `penpot.importImage is not a function` |
| `typography.setFont(font)` sobre un `LibraryTypography` | `t.setFont is not a function` |
**`penpot.importImage` (capturado en vivo, puerta 5 modo full y holdout, paso 6.4.25, 2026-08-03):**
variante distinta de la ya documentada `penpotUtils.importImage` — el modelo la invoca directamente
sobre el objeto `penpot` (no sobre `penpotUtils`), en 1 de los 10 prompts de la puerta 5 modo full
(`g5-03-card-producto`) y en 4 de los 60 del holdout, siempre en prompts que piden "una foto real
traída de una URL". El único seed de recuperación existente (grupo D) enseña la variante
`penpotUtils.importImage`, no ésta — de ahí que el modelo no generalizara la lección.
Tres casos distintos, y la diferencia importa pedagógicamente:
- `penpotUtils.importImage` **nunca existió**: no está en la Captura 1 de `penpot_api_docs.md`, ni
`import_image` está entre las herramientas MCP de este deployment (ver `data/schemas/penpot.json`).
El overview la menciona en prosa (`Use the export_shape and import_image tools`), lo cual es un
**anzuelo real**: la lista de herramientas que recibe el modelo es autoritativa, la prosa del
overview no. La ruta correcta es `await penpot.uploadMediaUrl(name, url)`.
- `typography.setFont` **sí está en el tipo** (`penpot_api_docs.md` Captura 26) pero **no existe en el
runtime de esta versión**. Es el caso canónico de "la documentación no es el runtime": hay que setear
las propiedades de tipografía una por una (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`).
Notar que el mensaje dice `t.setFont`, con el nombre de la variable local — no `typography.setFont`.
---
## 3. Uso de un retorno `null` sin chequear
La familia de errores más frecuente y la que produce la falla reportada por el usuario. Todas las
llamadas de creación y de búsqueda pueden devolver `null`; usar el resultado sin chequearlo produce un
`TypeError` en la **línea siguiente**, lo que hace que el stack apunte al síntoma y no a la causa.
| Disparador | Mensaje verbatim |
| --- | --- |
| `const t = penpot.createText(); t.characters = 'Hola'` | `Cannot set properties of null (setting 'characters')` |
| `const s = penpotUtils.findShapeById(page, id); s.x = 10` | `Cannot read properties of null (reading 'x')` |
| `const s = penpotUtils.findShapeById(idInexistente); return s.fills` | `Cannot read properties of null (reading 'fills')` |
| `const b = penpot.createBoolean('union', []); b.name = 'x'` | `Cannot set properties of null (setting 'name')` |
| `const r = penpot.createRectangle(); const f = r.addFlexLayout(); f.dir = 'row'` | `Cannot set properties of null (setting 'dir')` |
**`addFlexLayout()` sobre un `Rectangle` (capturado en vivo, puerta 5 modo full, `g5-02-navbar-flex`,
2026-08-03):** `addFlexLayout()` solo existe en `Board`. Llamarlo sobre un `Rectangle` (o sobre un
`Board` que ya tiene un layout asignado) devuelve `null` en silencio en vez de lanzar; el error real
aparece recién en la línea siguiente al intentar setear una propiedad del layout inexistente. Hay que
chequear el retorno de `addFlexLayout()` antes de usarlo, igual que con `createText()`.
Forma del mensaje, generalizable:
`Cannot read properties of null (reading '<prop>')` y
`Cannot set properties of null (setting '<prop>')`.
**Las tres fuentes de `null` que los seeds deben enseñar a chequear:**
1. `penpot.createText()` **sin argumento** o con `''``null`. Con un string no vacío → `Text`.
El ejemplo de la documentación oficial (`penpot_api_docs.md` Captura 6) usa la forma sin argumento.
2. `penpotUtils.findShapeById(page, id)`**la forma de dos argumentos no lanza**: devuelve `null` en
silencio, porque `findShapeById` tiene aridad 1 y el segundo argumento se descarta mientras el
primero (un `Page`, no un `string`) no matchea ningún id. La forma correcta es
`penpotUtils.findShapeById(id)`.
3. `penpot.createBoolean(type, shapes)` y `penpot.createShapeFromSvg(svg)` declaran `null | T` en su
firma (Capturas 10 y 11).
---
## 4. Fallos de red y de subida de medios
| Disparador | Mensaje verbatim |
| --- | --- |
| `await penpot.uploadMediaUrl('foto', 'https://dominio-inexistente.invalid/a.jpg')` | `Error uploading media` |
`uploadMediaUrl` devuelve una `Promise`; el fallo llega como rechazo, así que hay que envolverlo en
`try/catch` **con `await` adentro del `try`** (un `.catch()` colgado de la promesa sin `await` deja el
resto del payload corriendo con `undefined`).
---
## 5. Errores estándar del runtime de JavaScript
Deterministas dada la expresión, no específicos de Penpot. Se listan porque aparecen en los seeds del
grupo D y el lint los tiene que aceptar.
| Disparador | Mensaje verbatim |
| --- | --- |
| `undefined.foo` | `Cannot read properties of undefined (reading 'foo')` |
| `shapes.map(...)` donde `shapes` es `null` | `Cannot read properties of null (reading 'map')` |
| `JSON.parse('{')` | `Unexpected end of JSON input` |
| `board.appendChild()` sin argumento | `Cannot read properties of undefined (reading 'id')` |
---
## 6. Errores a nivel de servidor MCP (no de `execute_code`)
No son excepciones de JavaScript: los devuelve el servidor MCP antes de llegar al plugin. **No pueden
aparecer como resultado de un `execute_code` en un seed**, porque no dejan al modelo en una situación
de "diagnosticá tu código" — significan que no hay con qué hablar.
| Situación | Mensaje verbatim |
| --- | --- |
| El plugin de Penpot no está abierto / no está conectado al servidor MCP | `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.` |
Capturado en vivo el 2026-07-30 durante el paso 6.1.3 de esta fase.
---
## 7. Fallos SILENCIOSOS — no producen ningún error
**La categoría más peligrosa**, y la razón por la que el invariante R8 (`leer de vuelta el efecto`)
existe. Estas operaciones **no lanzan**, **no devuelven `null`**, y dejan el diseño mal. Un modelo que
solo maneja excepciones no las detecta nunca.
| Operación | Comportamiento real | Cómo detectarlo |
| --- | --- | --- |
| `flex.horizontalSizing = 'auto'` | Se lee de vuelta como `'auto'` pero **el board no crece** (200×200 sigue 200×200 tras insertar un hijo de 300 de ancho). Bug conocido penpot#8520 | Leer `board.width` tras insertar; hacer `board.resize()` a mano |
| `layoutChild.zIndex = 10` | Se lee de vuelta como `0`. **Se ignora** | Leer `layoutChild.zIndex`; usar el orden del array `children` para z-order |
| `text.letterSpacing = '-0.02'` | Se lee de vuelta como `"0"`. Los valores negativos en unidades tipo `em` **se descartan** | Leer `text.letterSpacing`; usar unidades tipo px (`'-1'`) |
| `penpot.createRectangle()` sin asignar `fills` | Queda con el fill por defecto **`#B1B2B5`** — el gris exacto del problema reportado | Invariante R1: `fills` explícito en todo shape creado |
| `penpotUtils.findShapeById(page, id)` | Devuelve `null`, **sin lanzar** | Chequear el retorno antes de usarlo |
| `board.layout` | `undefined`; `'layout' in shape === false`. La propiedad **no existe** | Usar `board.flex` / `board.grid`, que devuelven `null` cuando no hay layout |
| `generateStyle(shapes, {withChildren: true})` | La opción real es `includeChildren`; `withChildren` **se ignora** y el CSS sale sin los hijos | Ver `penpot_api_docs.md` Captura 8 |
| `grid.appendChild(shape, 0, 0)` | El `0` **se clampea a 1**: se lee de vuelta `[1,1]`. Los índices son 1-based | Leer `shape.layoutCell.row` / `.column` |
| `text.resize(w, h)` | Setea `growType` a `'fixed'` en silencio; el texto desborda su caja | Invariante R6: restaurar `growType` tras `resize()` |
| Leer `text.width` justo después de setear `characters` | El auto-sizing **no es inmediato**: se lee `1` | Dormir ~120 ms antes de leer el bounding box |