Phase 6: train a second LoRA for real Penpot UI design capability #5

Merged
aleleba merged 36 commits from agente-fase6-lora2-penpot into master 2026-08-04 21:36:07 -06:00
4 changed files with 3054 additions and 0 deletions
Showing only changes of commit d9629c44ae - Show all commits
+416
View File
@@ -0,0 +1,416 @@
# PENPOT_API_VERIFIED — allow-list de la API de Penpot
**Este archivo es el contrato.** Ningún seed de `data/raw/seeds/penpot.jsonl` puede usar un miembro de
la API de Penpot que no esté listado acá. `scripts/07_lint_penpot_code.py` lo verifica con hard-fail
sobre cada payload de `code`.
Si un seed necesita un miembro ausente de esta lista: **primero** se verifica contra `penpot_api_info`,
se agrega la captura verbatim a `penpot_api_docs.md`, y **después** se agrega acá con su evidencia.
Nunca al revés.
**Dos fuentes de verdad, y no coinciden.** La columna "documentado" es lo que dice el servidor
(`penpot_api_docs.md`); la columna "verificado" es lo que hace el runtime. Cuando difieren, **manda el
runtime**, y la discrepancia se anota explícitamente porque es material de entrenamiento: los seeds del
grupo C enseñan justamente a no confiar ciegamente en la documentación.
**Procedencia de las verificaciones en vivo:** sondeo de la sesión madre contra el archivo real del
usuario el 2026-07-30, creando shapes de prueba que se borraron al terminar (el archivo quedó en su
estado original: 0 shapes, 0 colores/tipografías/componentes en la biblioteca local, verificado con
`penpotUtils.shapeStructure(penpot.root, 2)`). Registrado en `PLAN.md` § "La API verificada" y en la
página de Docmost de la Fase 6.
---
## 0. Herramientas MCP disponibles — lista cerrada
Estas cuatro y **ninguna más**. Copiadas de `data/schemas/penpot.json`, que es byte a byte lo que el
modelo ve en inferencia.
| Herramienta | Parámetros | Requeridos |
| --- | --- | --- |
| `execute_code` | `code` | `code` |
| `export_shape` | `shapeId`, `format` (`svg`\|`png`, default `png`), `mode` (`shape`\|`fill`, default `shape`) | `shapeId` |
| `high_level_overview` | (ninguno) | — |
| `penpot_api_info` | `type`, `member` | `type` |
**Prohibido y sondeado por el holdout:**
- `import_image`**no existe en este deployment**. El overview la menciona en prosa; la lista de
herramientas es autoritativa. Ruta correcta: `await penpot.uploadMediaUrl(name, url)`.
- `export_shape` con `filePath` — el parámetro está eliminado del schema en este deployment
(ver `PENPOT_DEPLOYMENT_NOTES.md`). Inventarlo es invención de parámetros.
- `export_shape` con `scale``scale` es propiedad del método de plugin `shape.export(config)`
(`penpot_api_docs.md` Captura 29), **no** de la herramienta MCP. No hay forma de pedir 2x.
- Llamar `high_level_overview` dos veces — su propia descripción lo prohíbe
(`If you have already read the 'Penpot High-Level Overview', you must not call this tool.`).
Por eso aparece en exactamente **2** de los 96 seeds.
---
## 1. El objeto `penpot` — miembros permitidos
Lista cerrada, de `penpot_api_docs.md` Captura 2.
**Creación:** `createBoard()`, `createRectangle()`, `createEllipse()`, `createPath()`,
`createText(text)`, `createBoolean(boolType, shapes)`, `createShapeFromSvg(svgString)`,
`createShapeFromSvgWithImages(svgString)`, `createPage()`.
**Medios:** `uploadMediaUrl(name, url)`, `uploadMediaData(name, data, mimeType)`.
**Estructura y navegación:** `root`, `currentPage`, `currentFile`, `selection`, `openPage(page)`,
`group(shapes)`, `ungroup(group, ...other)`, `flatten(shapes)`.
**Alineación:** `alignHorizontal(shapes, dir)`, `alignVertical(shapes, dir)`,
`distributeHorizontal(shapes)`, `distributeVertical(shapes)`.
**Generación:** `generateMarkup(shapes, {type})`, `generateStyle(shapes, {type, withPrelude, includeChildren})`,
`generateFontFaces(shapes)`.
**Color:** `shapesColors(shapes)`, `replaceColor(shapes, oldColor, newColor)`.
**Contextos:** `library`, `fonts`, `history`, `viewport`, `theme`, `localStorage`, `utils`,
`currentUser`, `activeUsers`.
### PROHIBIDO — no existe
| Miembro inventado | Realidad |
| --- | --- |
| `penpot.createImage()` | No existe. Las imágenes van como `fills = [{fillOpacity: 1, fillImage: imageData}]` |
| `penpot.createComponent()` | Es `penpot.library.local.createComponent(shapes)` |
| `penpot.findShapeById()` | Es `penpotUtils.findShapeById(id)` o `page.getShapeById(id)` |
| cualquier miembro ausente de la Captura 2 | — |
---
## 2. `penpotUtils` — el objeto que inyecta el servidor MCP
Lista cerrada, de `penpot_api_docs.md` Captura 1. **Distinto de `penpot.utils`** (`ContextUtils`,
Captura 34), que solo tiene `geometry` y `types`.
| Firma | Aridad | Nota |
| --- | --- | --- |
| `getPages()` | 0 | `{id, name}[]` |
| `getPageById(id)` | 1 | |
| `getPageByName(name)` | 1 | |
| `shapeStructure(shape, maxDepth?)` | 1-2 | Devuelve `{id, name, type, children?, layout?}` |
| **`findShapeById(id)`** | **1** | **Ver § 3.1. La forma de 2 argumentos es el bug #1 del diagnóstico** |
| `findShape(predicate, root?)` | 1-2 | Sin `root` busca en todas las páginas |
| `findShapes(predicate, root?)` | 1-2 | |
| `isContainedIn(shape, container)` | 2 | |
| `setParentXY(shape, parentX, parentY)` | 3 | `parentX`/`parentY` son read-only, esta es la única vía |
| `analyzeDescendants(root, evaluator, maxDepth?)` | 2-3 | Devuelve `{shape, result}[]` |
### PROHIBIDO
`penpotUtils.importImage(...)` — no existe (`penpotUtils.importImage is not a function`).
---
## 3. Los cuatro errores que esta fase corrige
### 3.1 `findShapeById` tiene aridad 1
| | |
| --- | --- |
| **Documentado** | `findShapeById(id: string): Shape \| null` (Captura 1) |
| **Verificado** | `penpotUtils.findShapeById.length === 1`. `findShapeById(id)``{found: true}`. `findShapeById(page, id)`**`{found: false}`, sin lanzar** |
| **Correcto** | `const s = penpotUtils.findShapeById(id); if (!s) { ... }` |
| **PROHIBIDO** | `penpotUtils.findShapeById(page, id)`**27 ocurrencias sobre 21 de los 41 seeds viejos** |
El fallo es **silencioso**: devuelve `null` y revienta en la línea siguiente con
`Cannot read properties of null (reading '<prop>')`. El stack apunta al síntoma, no a la causa.
**Familia de búsqueda completa y permitida:**
| Forma | Cuándo |
| --- | --- |
| `penpotUtils.findShapeById(id)` | Id conocido, búsqueda global |
| `page.getShapeById(id)` | Id conocido, dentro de una página concreta |
| `penpotUtils.findShape(pred, root?)` | Primer match por predicado |
| `penpotUtils.findShapes(pred, root?)` | Todos los matches por predicado |
| `page.findShapes({name, nameLike, type})` | Búsqueda por criterio declarativo (Captura 23) |
| `penpotUtils.getPageByName(name)` | Obtener la página primero |
### 3.2 `shape.layout` no existe
| | |
| --- | --- |
| **Documentado** | `Board` declara `grid?: GridLayout` y `flex?: FlexLayout`. **No hay `layout`** (Captura 3) |
| **Verificado** | `'layout' in shape === false`; `shape.layout === undefined`. `board.flex` y `board.grid` devuelven **`null`** (no `undefined`) cuando no hay layout |
| **Correcto** | `if (board.flex) { board.flex.dir = 'column'; }` |
| **PROHIBIDO** | `shape.layout`, `board.layout`, `!!form.layout` — 8 ocurrencias sobre 5 seeds viejos |
**Origen del error, y por qué hay un seed dedicado a desambiguarlo:** la salida de
`penpotUtils.shapeStructure()` **sí** trae una clave `layout` (`{id, name, type, children?, layout?}`).
Es una clave del **output del helper**, no una propiedad del shape. Confundirlas es exactamente el error
que hay que desaprender.
### 3.3 `createText()` sin argumento devuelve `null`
| | |
| --- | --- |
| **Documentado** | Contrato: `createText(text: string): null \| Text`. **Ejemplo oficial: `text = penpot.createText();` sin argumento** (Captura 6) |
| **Verificado** | `createText('Hola mundo')``Text`. **`createText()``null`. `createText('')``null`** |
| **Correcto** | `const t = penpot.createText('Margherita'); if (!t) throw new Error('createText devolvió null');` |
| **PROHIBIDO** | `penpot.createText()` y `penpot.createText('')` |
Un modelo que consulta la documentación y sigue el ejemplo obtiene `null`, no puede crear texto, y
abandona: quedan solo rectángulos. Ésa es la mitad del síntoma reportado.
### 3.4 El gris `#B1B2B5`
| | |
| --- | --- |
| **Verificado** | El fill por defecto de `penpot.createRectangle()` es **`#B1B2B5`**. El de `createBoard()` es blanco |
| **Correcto** | Invariante R1: todo shape creado recibe `fills` explícito **antes** de insertarse |
El único ejemplo de flex layout en los 41 seeds viejos crea un board de 400×60 con dos rectángulos a los
que **nunca** asigna `fills` — el ejemplo canónico de "armar un layout" en los datos de entrenamiento
produce exactamente dos cajas grises.
---
## 4. Texto
| Miembro | Estado | Detalle verificado |
| --- | --- | --- |
| `characters` | ✅ | El contenido renderizado |
| `fontSize` | ✅ **string** | `'48'`. Los números se coercionan, pero el lint exige el string |
| `fontWeight` | ✅ **string** | `'700'` |
| `lineHeight` | ✅ **string** | `'1.2'` |
| `letterSpacing` | ✅ **string** | `'-1'` (px). **`'-0.02'` se lee de vuelta como `"0"`: los negativos en em se ignoran en silencio** |
| `fontFamily` | ✅ | `text.fontFamily = 'Work Sans'` como string directo **bindea** (`fontId: "gfont-work-sans"`). 1914 fuentes disponibles |
| `growType` | ✅ | `'auto-width'` \| `'auto-height'` \| `'fixed'`. Default `'auto-width'` |
| `align` | ✅ | `'left'` \| `'center'` \| `'right'` \| `'justify'` |
| `verticalAlign` | ✅ | `'top'` \| `'center'` \| `'bottom'` |
| `fills` | ✅ | El color del texto va acá. Default `[{fillColor:"#000000", fillOpacity:1}]` |
| `textTransform`, `textDecoration`, `direction` | ✅ | Captura 4 |
| `getRange(start, end)` | ✅ | Devuelve `TextRange` |
| `applyTypography(typography)` | ✅ | |
| **`textAlign`** | ❌ **LANZA** | `Cannot add property textAlign, object is not extensible`. **Mata todo el `execute_code`.** Usar `align` |
| **`color`** | ❌ **LANZA** | El objeto no es extensible. Usar `fills` |
| `font.applyToText(text)` | ⚠️ | Funciona, pero deja `fontId` como un objeto UUID de ClojureScript filtrado. **Preferir `fontFamily` como string** |
**Defaults de un `Text` recién creado, verificados:** `fontSize:"14"`, `fontFamily:"sourcesanspro"`,
`fontWeight:"400"`, `fills:[{fillColor:"#000000",fillOpacity:1}]`, `growType:"auto-width"`,
`width/height = [1, 1]`.
**Dimensionado (invariantes R4/R5/R6):**
- El auto-sizing **no es inmediato**: dormir ~120 ms antes de leer el bounding box.
- `resize(w, h)` setea `growType` a `'fixed'` **en silencio**. Restaurarlo si se quiere auto-sizing.
- `width` y `height` son **read-only**; `resize()` es la única vía.
- `parentX`/`parentY`/`boardX`/`boardY`/`bounds` son **read-only**; usar `penpotUtils.setParentXY()`.
---
## 5. Fills, gradientes, strokes, sombras, radios
| Miembro | Estado | Detalle |
| --- | --- | --- |
| `fills = [{fillColor, fillOpacity}]` | ✅ | |
| `fills` apilados | ✅ | Foto + scrim oscuro encima funciona |
| `fillColorGradient` | ✅ | Coords **normalizadas 0..1** con `width: 1`, aceptado verbatim |
| `fillImage` | ✅ | `fills = [{fillOpacity: 1, fillImage: img}]`; `img.keepAspectRatio = true` para fotos |
| `fillColorRefFile` / `fillColorRefId` | ✅ | Los pone `libraryColor.asFill()` |
| `strokes = [{strokeColor, strokeWidth, strokeStyle, strokeAlignment, strokeOpacity}]` | ✅ | |
| `borderRadius`, `borderRadiusTopLeft`… | ✅ | En rectángulo y board |
| `shadows = [{style, offsetX, offsetY, blur, spread, color}]` | ✅ | |
| **`Shadow.color`** | ⚠️ | Es un **`Color`** (`{color: '#000000', opacity: 0.12}`), **no un `Fill`**. `{fillColor: ...}` es el bug clásico |
| `blur` | ✅ | |
| `opacity`, `blendMode` | ✅ | |
**Gradiente, forma verificada:**
```js
fills = [{ fillOpacity: 1, fillColorGradient: {
type: 'linear', startX: 0, startY: 0, endX: 0, endY: 1, width: 1,
stops: [{ color: '#D62828', offset: 0 }, { color: '#F77F00', offset: 1 }]
}}]
```
---
## 6. Jerarquía e inserción
| Situación | Forma correcta |
| --- | --- |
| Padre **sin** layout | `parent.insertChild(parent.children.length, shape)` |
| Board **con** flex | `board.appendChild(shape)`, llamado **en orden visual** |
| Board **con** grid | `board.grid.appendChild(shape, row, column)`**1-based** |
| Índice específico en flex | `board.insertChild(index, shape)`, recordando el orden invertido |
| Reparentar | `newParent.appendChild(shape)` / `insertChild(...)` — quita del padre viejo, preserva x/y absolutos |
| **PROHIBIDO** | `board.flex.appendChild(shape)`**está roto** |
**Orden invertido, verificado.** En `dir: 'column'` (y `'row'`), apendeando PRIMERO y luego SEGUNDO, el
array `children` queda `[SEGUNDO, PRIMERO, ...]`: `board.appendChild` inserta **al frente**. Por eso hay
que llamarlo en orden visual.
**La regla absoluta "nunca `appendChild`" que enseñan los seeds viejos es INCORRECTA.** El overview la
califica él mismo (`except in flex layout boards`) y la sección de Flex Layout **manda** usar
`board.appendChild`. La política correcta es condicional, y hay 5 seeds del grupo A3 dedicados a ella.
**Z-order:** el orden del array `children`. Métodos: `bringToFront()`, `sendToBack()`,
`bringForward()`, `sendBackward()`, `setParentIndex(index)` (0-based).
---
## 7. Layouts
### Flex
| Miembro | Estado | Detalle |
| --- | --- | --- |
| `board.addFlexLayout()` | ✅ | Devuelve `FlexLayout` |
| `board.flex` | ✅ | Guard: `if (board.flex)`. **`null`** si no hay layout |
| `dir` | ✅ | `'row'` \| `'row-reverse'` \| `'column'` \| `'column-reverse'` |
| `rowGap`, `columnGap` | ✅ | **No existe el shorthand `gap`** |
| `alignItems`, `alignContent`, `justifyItems`, `justifyContent` | ✅ | |
| `topPadding`/`rightPadding`/`bottomPadding`/`leftPadding`, `verticalPadding`/`horizontalPadding` | ✅ | |
| `horizontalSizing` / `verticalSizing` | ⚠️ | `'fill'` \| `'auto'` \| `'fit-content'`. **`'auto'` se lee de vuelta como `'auto'` pero el board NO crece** (penpot#8520). Hay que `board.resize()` a mano |
| `wrap` | ✅ | `'wrap'` \| `'nowrap'` |
| `flex.appendChild` | ❌ | **Roto.** Usar `board.appendChild` |
### Grid
| Miembro | Estado | Detalle |
| --- | --- | --- |
| `board.addGridLayout()` | ✅ | **No `addFlexLayout()`**, como dice erróneamente el overview |
| `board.grid` | ✅ | Guard: `if (board.grid)` |
| `addRow(type, value?)`, `addColumn(type, value?)` | ✅ | `type`: `'flex'` \| `'fixed'` \| `'percent'` \| `'auto'` |
| `addRowAtIndex`, `addColumnAtIndex`, `removeRow`, `removeColumn`, `setRow`, `setColumn` | ✅ | |
| `rows`, `columns` | ✅ | `Track[]` = `{type, value}` |
| **`grid.appendChild(shape, row, column)`** | ✅ | **Índices 1-based.** `(s, 0, 0)` se clampea y se lee `[1,1]`; `(s, 1, 2)` se lee `[1,2]`. El ejemplo 0-based de la doc de tipos está mal |
| `shape.layoutCell` | ✅ | `{row, rowSpan, column, columnSpan, areaName, position}` — para leer de vuelta y reposicionar |
### `layoutChild` (el hijo dentro de un board con layout)
| Miembro | Estado | Detalle |
| --- | --- | --- |
| `layoutChild` | ✅ | **Existe solo DESPUÉS de insertar** el shape en un board con layout |
| `horizontalSizing` / `verticalSizing` | ✅ | `'fill'` \| `'auto'` \| **`'fix'`** (no `'fixed'`, no `'fit-content'`) |
| `alignSelf` | ✅ | |
| `absolute` | ✅ | `true` saca al hijo del flujo del layout |
| `*Margin`, `maxWidth`/`maxHeight`/`minWidth`/`minHeight` | ✅ | |
| **`zIndex`** | ⚠️ | Está en el tipo pero **se ignora en silencio** (se setea 10, se lee 0). Usar el orden de `children` |
**Invariante R8:** todo efecto de layout se **lee de vuelta** del objeto retornado. El seed se entrena
sobre `"titleStretched": true` leído del runtime, no sobre la esperanza de que haya funcionado.
**Invariante R9:** wrappers abrazan (`flex.horizontalSizing = 'fit-content'`), secciones llenan
(`layoutChild.horizontalSizing = 'fill'`). Los dos idioms en el mismo seed para que el contraste sea
aprendible.
---
## 8. Imágenes
| Miembro | Estado |
| --- | --- |
| `await penpot.uploadMediaUrl(name, url)``ImageData` | ✅ **Única vía** |
| `await penpot.uploadMediaData(name, bytes, mimeType)` | ✅ |
| `fills = [{fillOpacity: 1, fillImage: img}]` | ✅ |
| `img.keepAspectRatio = true` | ✅ Para fotos |
| `penpot.createImage()` | ❌ No existe |
| `penpotUtils.importImage()` | ❌ `penpotUtils.importImage is not a function` |
| herramienta MCP `import_image` | ❌ No existe en este deployment |
`uploadMediaUrl` devuelve una `Promise` y puede rechazar (`Error uploading media`): `await` dentro de
un `try/catch`.
---
## 9. Biblioteca local (tokens de diseño)
| Miembro | Estado | Detalle |
| --- | --- | --- |
| `penpot.library.local` | ✅ | |
| `library.local.createColor()` | ✅ | Luego `c.name = 'Brand/Primary'; c.color = '#D62828';` |
| `libraryColor.asFill()` | ✅ | Devuelve un `Fill` con `fillColorRefFile`/`fillColorRefId` |
| `libraryColor.asStroke()` | ✅ | |
| `library.local.createTypography()` | ✅ | |
| `library.local.createComponent(shapes)` | ✅ | |
| `component.instance()` / `mainInstance()` | ✅ | |
| `penpot.library.connected`, `availableLibraries()`, `connectLibrary(id)` | ✅ | |
| **`typography.setFont(font, variant?)`** | ❌ | Está en el tipo, **no existe en el runtime**: `t.setFont is not a function`. Setear `fontFamily`/`fontSize`/`fontWeight`/`lineHeight` una por una |
---
## 10. Verificación programática (invariante R12)
| Miembro | Estado | Uso |
| --- | --- | --- |
| `penpot.generateStyle(shapes, {type:'css', includeChildren:true})` | ✅ | Devuelve CSS real. **La opción es `includeChildren`; `withChildren` (que usa el overview) se ignora** |
| `penpot.generateMarkup(shapes, {type:'html'})` | ✅ | Devuelve HTML real |
| `penpotUtils.analyzeDescendants(root, evaluator, maxDepth?)` | ✅ | Auditoría; el evaluador puede devolver funciones correctoras |
| `penpotUtils.isContainedIn(shape, container)` | ✅ | Violaciones de contención |
| `penpotUtils.shapeStructure(shape, maxDepth)` | ✅ | Overview del árbol. **Su clave `layout` es del output, no del shape** |
| herramienta `export_shape` | ✅ | Inspección visual |
**R12: nunca terminar sin mirar y medir.** Todo seed compositivo cierra con `export_shape` + una
auditoría programática, y el mensaje final **cita los números** de esa auditoría.
---
## 11. Contramedida al system prompt del servidor
El servidor MCP inyecta verbatim (ver `penpot_system_prompt.md`):
> NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own
> colours and stick to non-creative defaults such as white/black if you are lacking information).
**Ninguno de los 41 seeds viejos tiene mensaje `system`**, así que el modelo nunca se entrenó en
presencia de esta instrucción. En producción la aplica al revés y produce el gris.
Obligatorio en los seeds nuevos:
1. El bloque `instructions` **verbatim** como mensaje `system` en **~30 %** de los seeds (el resto sin
él, para robustez en las dos condiciones).
2. En **todo** seed creativo, `reasoning_content` que desambigüe explícitamente. La regla condiciona
sobre *"transferring styles from a Penpot design to code"*; crear un diseño nuevo es el caso
contrario: no hay diseño previo del que transferir, así que no hay "missing values" sobre los que
asumir. Elegir una paleta deliberada **es** la tarea.
---
## 12. Los doce invariantes anti-caja-gris
Cada uno presente **en los datos** y verificado por `scripts/07_lint_penpot_code.py`, no solo enunciado
en prosa.
| # | Invariante |
| --- | --- |
| R1 | Todo shape creado recibe `fills` explícito **antes** de insertarse. Los wrappers puros de layout ponen `fills = []` — transparente **por intención**, nunca "sin tocar" |
| R2 | **Cero grises de placeholder.** Todo hex debe pasar `saturación > 10` **o** `max < 100` **o** `max > 220`. El gris solo aparece en los tool results del grupo B8, y solo como *input* |
| R3 | Toda paleta es un set cerrado y nombrado de ≤ 8 roles, declarado al inicio del payload y referenciado por rol |
| R4 | Todo texto recibe `characters` + tamaño + fill + `growType` |
| R5 | Tamaño, peso e interlineado son **strings** entre comillas |
| R6 | Tras `resize()` sobre un texto, restaurar `growType` |
| R7 | Una card es fondo + radio + elevación |
| R8 | Todo hijo de layout declara su sizing, y el efecto se **lee de vuelta** del objeto retornado |
| R9 | Wrappers abrazan (`fit-content`), secciones llenan (`fill`) — los dos idioms en el mismo seed |
| R10 | Escala tipográfica, nunca tamaños ad-hoc: ≥ 3 tamaños distintos por pantalla compuesta |
| R11 | Espaciado en escala de 8 px |
| R12 | Nunca terminar sin mirar y medir: `export_shape` → auditoría → resumen que **cita los números** |
---
## 13. Resumen de patrones prohibidos (lo que el lint busca)
| Patrón | Por qué |
| --- | --- |
| `findShapeById(` con coma en los argumentos | Aridad 1; la forma de 2 devuelve `null` en silencio |
| `.layout` en una línea sin `shapeStructure` | La propiedad no existe |
| `.flex.appendChild(` | Roto |
| `appendChild(` sobre un receptor sin evidencia de flex | Solo es correcto en boards con flex |
| `fontSize`/`fontWeight`/`lineHeight`/`letterSpacing` `= <número>` | Son strings |
| `textAlign =` | Lanza y mata el `execute_code` |
| `importImage`, `import_image`, `createImage(`, `filePath` | No existen en este deployment |
| Asignación a `width`/`height`/`parentX`/`parentY`/`bounds` | Read-only |
| `gap =` | No hay shorthand; son `rowGap`/`columnGap` |
| `shadows = [{... color: {fillColor` | `Shadow.color` es un `Color`, no un `Fill` |
| Hex gris de placeholder fuera del grupo B8 | Invariante R2 |
| `console.log` de algo que también se retorna | Prohibición explícita del servidor |
| `setFont(` | No existe en el runtime |
| `createText()` / `createText('')` | Devuelven `null` |
| `high_level_overview` en más de 2 seeds | Su descripción prohíbe llamarlo dos veces |
File diff suppressed because it is too large Load Diff
+154
View File
@@ -0,0 +1,154 @@
# 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 |
+60
View File
@@ -0,0 +1,60 @@
# Penpot MCP — bloque `instructions` del servidor, verbatim
Este archivo guarda **verbatim** el bloque `instructions` que el servidor MCP de Penpot inyecta en el
system prompt de todo cliente que se conecta. Es la **causa raíz #4** del diagnóstico de la Fase 6: la
regla de "don't get overly creative / don't pick your own colours" está pensada para el flujo
diseño→código, pero el modelo la aplica al revés y produce cajas grises cuando se le pide crear un
diseño con colores vibrantes.
**Procedencia:** capturado el 2026-07-30 en la Fase 6, contra el servidor MCP `penpot` conectado a esta
sesión. El texto es idéntico al que devuelve la herramienta `high_level_overview` (el servidor sirve el
mismo documento por ambas vías); la copia completa de esa captura vive en `penpot_api_docs.md`.
**Uso obligatorio:** este texto va **byte a byte** como mensaje `system` en ~30 % de los seeds nuevos de
`data/raw/seeds/penpot.jsonl`. Ninguna paráfrasis, ningún recorte. Ver `PENPOT_API_VERIFIED.md` §
"Contramedida al system prompt".
---
## Fragmento crítico (el que produce el gris)
> IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
> NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
> non-creative defaults such as white/black if you are lacking information).
Lectura correcta, y la que los seeds deben enseñar explícitamente en `reasoning_content`: la regla
**condiciona sobre "transferring styles from a Penpot design to code"**. Cuando el usuario pide *crear*
un diseño nuevo no hay diseño previo del que transferir, así que no hay "missing values" sobre los que
asumir: elegir una paleta deliberada es la tarea, no una invención.
---
## Bloque completo, verbatim
```
You have access to Penpot tools in order to interact with a Penpot design project directly.
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
non-creative defaults such as white/black if you are lacking information).
# Executing Code
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
directly in the connected project.
VERY IMPORTANT: When writing code, NEVER LOG INFORMATION YOU ARE ALSO RETURNING. It would duplicate the information you receive!
To execute code correctly, you need to understand the Penpot Plugin API. You can retrieve API documentation via
the `penpot_api_info` tool.
This is the full list of types/interfaces in the Penpot API: Penpot, ActiveUser, Blur, Board, VariantContainer, Boolean, CloseOverlay, Color, ColorShapeInfo, ColorShapeInfoEntry, Comment, CommentThread, CommonLayout, Context, ContextGeometryUtils, ContextTypesUtils, ContextUtils, Dissolve, Ellipse, EventsMap, Export, File, FileVersion, Fill, FlexLayout, Flow, Font, FontVariant, FontsContext, GridLayout, Group, GuideColumn, GuideColumnParams, GuideRow, GuideSquare, GuideSquareParams, HistoryContext, Image, Interaction, LayoutCellProperties, LayoutChildProperties, Library, LibraryColor, LibraryComponent, LibraryVariantComponent, LibraryElement, LibrarySummary, LibraryTypography, LocalStorage, NavigateTo, OpenOverlay, OpenUrl, OverlayAction, Page, Path, PathCommand, PluginData, PreviousScreen, Push, Rectangle, RulerGuide, Shadow, ShapeBase, Slide, Stroke, SvgRaw, Text, TextRange, ToggleOverlay, Track, User, Variants, Viewport, Action, Animation, BooleanType, Bounds, Gradient, Guide, ImageData, LibraryContext, Point, RulerGuideOrientation, Shape, StrokeCap, Theme, TrackType, Trigger
You use the `storage` object extensively to store data and utility functions you define across tool calls.
This allows you to inspect intermediate results while still being able to build on them in subsequent code executions.
```
El resto del documento (estructura de diseños, propiedades de shapes, z-order, layouts, texto,
`penpotUtils`, inspección visual, librerías) está capturado íntegro y verbatim en
`penpot_api_docs.md`, sección "Captura 1".