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.
9.1 KiB
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.importImagenunca existió: no está en la Captura 1 depenpot_api_docs.md, niimport_imageestá entre las herramientas MCP de este deployment (verdata/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 esawait penpot.uploadMediaUrl(name, url).typography.setFontsí está en el tipo (penpot_api_docs.mdCaptura 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 dicet.setFont, con el nombre de la variable local — notypography.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:
penpot.createText()sin argumento o con''→null. Con un string no vacío →Text. El ejemplo de la documentación oficial (penpot_api_docs.mdCaptura 6) usa la forma sin argumento.penpotUtils.findShapeById(page, id)— la forma de dos argumentos no lanza: devuelvenullen silencio, porquefindShapeByIdtiene aridad 1 y el segundo argumento se descarta mientras el primero (unPage, no unstring) no matchea ningún id. La forma correcta espenpotUtils.findShapeById(id).penpot.createBoolean(type, shapes)ypenpot.createShapeFromSvg(svg)declarannull | Ten 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 |