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).
177 lines
11 KiB
Markdown
177 lines
11 KiB
Markdown
# 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 |
|