The 41 existing Penpot seeds contain hand-fabricated penpot_api_info and high_level_overview tool results that assert facts the server never said, which is how the model learned an API that does not exist. This adds four schema files that make the seed corpus mechanically verifiable against the real server instead. - penpot_api_docs.md: 34 verbatim captures of high_level_overview and penpot_api_info, each headed by the exact request that produced it. Every penpot_api_info tool result in a seed must be a subset of lines of this file, in original order. Records three places where the served docs contradict the runtime (addFlexLayout/addGridLayout copy-paste in the Grid section, flex.appendChild for grid children, withChildren vs includeChildren), plus the createText() example that is the direct cause of the production failure. - penpot_system_prompt.md: the server's instructions block verbatim. Goes as a system message into ~30% of the new seeds; it is the countermeasure to the "don't pick your own colours" rule that produces the grey boxes. - penpot_errors.md: the real error strings, including a section on silent failures that raise nothing at all and are why the read-back invariant exists. - PENPOT_API_VERIFIED.md: the allow-list. No seed may reference a member absent from it. Documents the four root causes (findShapeById arity 1, no shape.layout, createText() returning null, the #B1B2B5 default fill), the twelve anti-grey-box invariants, and the forbidden-pattern list the linter checks. Live re-verification of the error strings is still pending: the Penpot plugin is not currently connected, so it is deferred to the gate 5 baseline step, which needs the live connection anyway.
155 lines
9.1 KiB
Markdown
155 lines
9.1 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` |
|
||
|
||
Forma del mensaje, generalizable: `Cannot add property <nombre>, object is not extensible`.
|
||
|
||
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` |
|
||
| `typography.setFont(font)` sobre un `LibraryTypography` | `t.setFont is not a function` |
|
||
|
||
Dos 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')` |
|
||
|
||
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 |
|