diff --git a/data/schemas/PENPOT_API_VERIFIED.md b/data/schemas/PENPOT_API_VERIFIED.md new file mode 100644 index 0000000..94dfab6 --- /dev/null +++ b/data/schemas/PENPOT_API_VERIFIED.md @@ -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 '')`. 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` `= ` | 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 | diff --git a/data/schemas/penpot_api_docs.md b/data/schemas/penpot_api_docs.md new file mode 100644 index 0000000..155b851 --- /dev/null +++ b/data/schemas/penpot_api_docs.md @@ -0,0 +1,2424 @@ +# Penpot MCP — documentación de API capturada verbatim + +Capturas **literales** de las herramientas de solo lectura del servidor MCP `penpot`, tomadas el +2026-07-30 durante la Fase 6 contra el mismo servidor que ve el modelo en inferencia. + +**Para qué existe este archivo.** Los 41 seeds viejos de `data/raw/seeds/penpot.jsonl` contenían +resultados de `penpot_api_info` y `high_level_overview` **fabricados a mano**, que afirmaban hechos +falsos como si los hubiera dicho el servidor (p. ej. un doc string inventado declarando +`Properties: layout?: FlexLayout | GridLayout`). Eso entrena al modelo a confiar en documentación que +no existe. La política de la Fase 6 es mecánicamente verificable: + +> **Todo resultado de un mensaje `tool` de `penpot_api_info` o `high_level_overview` en un seed debe ser +> un subconjunto de líneas de este archivo, en su orden original, sin reformular.** + +`scripts/07_lint_penpot_code.py` lo verifica con hard-fail. Si un seed necesita un miembro que no está +capturado acá, primero se captura contra el servidor y se agrega a este archivo con su request exacto. + +**Cada bloque está encabezado por el request exacto que lo produjo.** No editar el contenido de los +bloques cercados: son la fuente de verdad. + +--- + +## Captura 1 — `high_level_overview()` + +Request: + +```json +{"tool": "high_level_overview", "arguments": {}} +``` + +Salida 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. + +# The Structure of Penpot Designs + +A Penpot design ultimately consists of shapes. +The type `Shape` is a union type, which encompasses both containers and low-level shapes. +Shapes in a Penpot design are organized hierarchically. +At the top level, a design project contains one or more `Page` objects. +Each `Page` contains a tree of elements. For a given instance `page`, its root shape is `page.root`. +A Page is frequently structured into boards. A `Board` is a high-level grouping element. +A `Group` is a more low-level grouping element used to organize low-level shapes into a logical unit. +Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`, `Boolean`, and `SvgRaw`. +`ShapeBase` is a base type most shapes build upon. + +# Core Shape Properties and Methods + +**Type**: + Any given shape contains information on the concrete type via its `type` field. + +**Position and Dimensions**: + * The location properties `x` and `y` refer to the top left corner of a shape's bounding box in the absolute (Page) coordinate system. + These are writable - set them directly to position shapes. + * `parentX` and `parentY` (as well as `boardX` and `boardY`) are READ-ONLY computed properties showing position relative to parent/board. + To position relative to parent, use `penpotUtils.setParentXY(shape, parentX, parentY)` or manually set `shape.x = parent.x + parentX`. + * `width` and `height` are READ-ONLY. Use `resize(width, height)` method to change dimensions. + * `bounds` is a READ-ONLY property. Use `x`, `y` with `resize()` to modify shape bounds. + +**Other Writable Properties**: + * `name` - Shape name + * `fills`, `strokes` - Styling properties + * `rotation`, `opacity`, `blocked`, `hidden`, `visible` + +**Z-Order**: + * The z-order of shapes is determined by the order in the `children` array of the parent shape. + Therefore, when creating shapes that should be on top of each other, add them to the parent in the correct order + (i.e. add background shapes first, then foreground shapes later). + CRITICAL: NEVER use the broken function `appendChild` to achieve this, ALWAYS use `parent.insertChild(parent.children.length, shape)` + * To modify z-order after creation, use these methods: `bringToFront()`, `sendToBack()`, `bringForward()`, `sendBackward()`, + and, for precise control, `setParentIndex(index)` (0-based). + +**Modification Methods**: + * `resize(width, height)` - Change dimensions (required for width/height since they're read-only) + * `rotate(angle, center?)` - Rotate shape + * `remove()` - Permanently destroy the shape (use only for deletion, NOT for reparenting) + +**Hierarchical Structure**: + * `parent` - The parent shape (null for root shapes) + Note: Hierarchical nesting does not necessarily imply visual containment + * CRITICAL: To add children to a parent shape (e.g. a `Board`): + - ALWAYS use `parent.insertChild(index, shape)` to add a child, e.g. `parent.insertChild(parent.children.length, shape)` to append + - NEVER use `parent.appendChild(shape)` as it is BROKEN and will not insert in a predictable place (except in flex layout boards) + * Reparenting: `newParent.appendChild(shape)` or `newParent.insertChild(index, shape)` will move a shape to new parent + - Automatically removes the shape from its old parent + - Absolute x/y positions are preserved (use `penpotUtils.setParentXY` to adjust relative position) + +# Images + +The `Image` type is a legacy type. Images are now typically embedded in a `Fill`, with `fillImage` set to an +`ImageData` object, i.e. the `fills` property of of a shape (e.g. a `Rectangle`) will contain a fill where `fillImage` is set. +Use the `export_shape` and `import_image` tools to export and import images. + +# Layout Systems + +Boards can have layout systems that automatically control the positioning and spacing of their children: + + * **Flex Layout**: A flexbox-style layout system + - Add to a board with `board.addFlexLayout(): FlexLayout`; instance then accessibly via `board.flex`; + Check with: `if (board.flex) { ... }` + - Properties: `dir`, `rowGap`, `columnGap`, `alignItems`, `justifyContent`; + - `dir`: "row" | "column" | "row-reverse" | "column-reverse" + - Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding` + - When a board has flex layout, + - child positions are controlled by the layout system, not by individual x/y coordinates; + appending or inserting children automatically positions them according to the layout rules. + - CRITICAL: For for dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order! + Therefore, the element that appears first in the array, appears visually at the end (bottom/right) and vice versa. + ALWAYS BEAR IN MIND THAT THE CHILDREN ARRAY ORDER IS REVERSED FOR dir="column" OR dir="row"! + - CRITICAL: The FlexLayout method `board.flex.appendChild` is BROKEN. To append children to a flex layout board such that + they appear visually at the end, ALWAYS use the Board's method `board.appendChild(shape)`; it will insert at the front + of the `children` array for dir="column" or dir="row", which is what you want. So call it in the order of visual appearance. + To insert at a specific index, use `board.insertChild(index, shape)`, bearing in mind the reversed order for dir="column" + or dir="row". + - To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions + + * **Grid Layout**: A CSS grid-style layout system + - Add to a board with `board.addFlexLayout(): FlexLayout`; instance then accessibly via `board.grid`; + Check with: `if (board.grid) { ... }` + - Properties: `rows`, `columns`, `rowGap`, `columnGap` + - Children are positioned via 1-based row/column indices + - Add to grid via `board.flex.appendChild(shape, row, column)` + - Modify grid positioning after the fact via `shape.layoutCell: LayoutCellProperties` + + * When working with boards: + - ALWAYS check if the board has a layout system before attempting to reposition children + - Modify layout properties (gaps, padding) instead of trying to set child x/y positions directly + - Layout systems override manual positioning of children + +# Text Elements + +The rendered content of `Text` element is given by the `characters` property. + +To change the size of the text, change the `fontSize` property; applying `resize()` does NOT change the font size, +it only changes the formal bounding box; if the text does not fit it, it will overflow. +The bounding box is sized automatically as long as the `growType` property is set to "auto-width" or "auto-height". +`resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-*" if you want automatic sizing - otherwise the bounding box will be meaningless, with the text overflowing! +The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box. + +# The `penpot` and `penpotUtils` Objects, Exploring Designs + +A key object to use in your code is the `penpot` object (which is of type `Penpot`): + * `penpot.selection` provides the list of shapes the user has selected in the Penpot UI. + If it is unclear which elements to work on, you can ask the user to select them for you. + ALWAYS immediately copy the selected shape(s) into `storage`! Do not assume that the selection remains unchanged. + * `penpot.root` provides the root shape of the currently active page. + * Generation of CSS content for elements via `penpot.generateStyle` + * Generation of HTML/SVG content for elements via `penpot.generateMarkup` + +For example, to generate CSS for the currently selected elements, you can execute this: + return penpot.generateStyle(penpot.selection, { type: "css", withChildren: true }); + +CRITICAL: The `penpotUtils` object provides essential utilities - USE THESE INSTEAD OF WRITING YOUR OWN: + * getPages(): { id: string; name: string }[] + * getPageById(id: string): Page | null + * getPageByName(name: string): Page | null + * shapeStructure(shape: Shape, maxDepth: number | undefined = undefined): { id, name, type, children?, layout? } + Generates an overview structure of the given shape. + - children: recursive, limited by maxDepth + - layout: present if shape has flex/grid layout, contains { type: "flex" | "grid", ... } + * findShapeById(id: string): Shape | null + * findShape(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape | null + If no root is provided, search globally (in all pages). + * findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape[] + * isContainedIn(shape: Shape, container: Shape): boolean + Returns true iff shape is fully within the container's geometric bounds. + Note that a shape's bounds may not always reflect its actual visual content - descendants can overflow; check using analyzeDescendants (see below). + * setParentXY(shape: Shape, parentX: number, parentY: number): void + Sets shape position relative to its parent (since parentX/parentY are read-only) + * analyzeDescendants(root: Shape, evaluator: (root: Shape, descendant: Shape) => T | null | undefined, maxDepth?: number): Array<{ shape: Shape, result: T }> + General-purpose utility for analyzing/validating descendants + Calls evaluator on each descendant; collects non-null/undefined results + Powerful pattern: evaluator can return corrector functions or diagnostic data + +General pointers for working with Penpot designs: + * Prefer `penpotUtils` helper functions — avoid reimplementing shape searching. + * To get an overview of a single page, use `penpotUtils.shapeStructure(page.root, 3)`. + Note that `penpot.root` refers to the current page only. When working across pages, first determine the relevant page(s). + * Use `penpotUtils.findShapes()` or `penpotUtils.findShape()` with predicates to locate elements efficiently. + +Common tasks - Quick Reference (ALWAYS use penpotUtils for these): + * Find all images: + const images = penpotUtils.findShapes( + shape => shape.type === 'image' || shape.fills?.some(fill => fill.fillImage), + penpot.root + ); + * Find text elements: + const texts = penpotUtils.findShapes(shape => shape.type === 'text', penpot.root); + * Find (the first) shape with a given name: + const shape = penpotUtils.findShape(shape => shape.name === 'MyShape'); + * Get structure of current selection: + const structure = penpotUtils.shapeStructure(penpot.selection[0]); + * Find shapes in current selection/board: + const shapes = penpotUtils.findShapes(predicate, penpot.selection[0] || penpot.root); + * Validate/analyze descendants (returning corrector functions): + const fixes = penpotUtils.analyzeDescendants(board, (root, shape) => { + const xMod = shape.parentX % 4; + if (xMod !== 0) { + return () => penpotUtils.setParentXY(shape, Math.round(shape.parentX / 4) * 4, shape.parentY); + } + }); + fixes.forEach(f => f.result()); // Apply all fixes + * Find containment violations: + const violations = penpotUtils.analyzeDescendants(board, (root, shape) => { + return !penpotUtils.isContainedIn(shape, root) ? 'outside-bounds' : null; + }); + Always validate against the root container that is supposed to contain the shapes. + +# Visual Inspection of Designs + +For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose! + +# Revising Designs + +* Before applying design changes, ask: "Would a designer consider this appropriate?" +* When dealing with containment issues, ask: Is the parent too small OR is the child too large? + Container sizes are usually intentional, check content first. +* Check for reasonable font sizes and typefaces + +# Asset Libraries + +Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files. +They enable design systems and consistent styling across projects. +Each Penpot file has its own local library and can connect to external shared libraries. + +Accessing libraries: via `penpot.library` (type: `LibraryContext`): + * `penpot.library.local` (type: `Library`) - The current file's own library + * `penpot.library.connected` (type: `Library[]`) - Array of already-connected external libraries + * `penpot.library.availableLibraries()` (returns: `Promise`) - Libraries available to connect + * `penpot.library.connectLibrary(libraryId: string)` (returns: `Promise`) - Connect a new library + +Each `Library` object has: + * `id: string` + * `name: string` + * `components: LibraryComponent[]` - Array of components + * `colors: LibraryColor[]` - Array of colors + * `typographies: LibraryTypography[]` - Array of typographies + +Using library components: + * find a component in the library by name: + const component: LibraryComponent = library.components.find(comp => comp.name.includes('Button')); + * create a new instance of the component on the current page: + const instance: Shape = component.instance(); + This returns a `Shape` (often a `Board` containing child elements). + After instantiation, modify the instance's properties as desired. + * get the reference to the main component shape: + const mainShape: Shape = component.mainInstance(); + +Adding assets to a library: + * const newColor: LibraryColor = penpot.library.local.createColor(); + newColor.name = 'Brand Primary'; + newColor.color = '#0066FF'; + * const newTypo: LibraryTypography = penpot.library.local.createTypography(); + newTypo.name = 'Heading Large'; + // Set typography properties... + * const shapes: Shape[] = [shape1, shape2]; // shapes to include + const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes); + newComponent.name = 'My Button'; + +-- +You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again. +``` + +### Advertencias sobre esta captura + +La captura es la fuente de verdad de **lo que el servidor dice**, no de **lo que el runtime hace**. Tres +afirmaciones de este documento están **desmentidas empíricamente** (ver `PENPOT_API_VERIFIED.md`): + +1. `Add to a board with 'board.addFlexLayout(): FlexLayout'; instance then accessibly via 'board.grid'` — + copy-paste erróneo en la sección de Grid Layout: el método real es `board.addGridLayout()`. +2. `Add to grid via 'board.flex.appendChild(shape, row, column)'` — es `board.grid.appendChild(...)`. +3. `penpot.generateStyle(penpot.selection, { type: "css", withChildren: true })` — la opción real se + llama `includeChildren`, no `withChildren` (ver Captura 8). `withChildren` se ignora en silencio. + +La afirmación `NEVER use parent.appendChild(shape) as it is BROKEN` está calificada por el propio +documento (`except in flex layout boards`) y por la sección de Flex Layout, que **manda** usar +`board.appendChild(shape)` en boards con flex. La regla absoluta "nunca appendChild" que enseñan los +seeds viejos es incorrecta. + +Y el ejemplo de `createText` (Captura 6) es la fuente directa de la falla de producción. + +--- + +## Captura 2 — `penpot_api_info {"type": "Penpot"}` + +``` +Interface Penpot +================ + +These are methods and properties available on the `penpot` global object. + +``` +interface Penpot { + ui: { + open: ((name: string, url: string, options?: { + width: number; + height: number; + }) => void); + size: null | { + width: number; + height: number; + }; + resize: ((width: number, height: number) => void); + sendMessage: ((message: unknown) => void); + onMessage: ((callback: ((message: T) => void)) => void); + }; + utils: ContextUtils; + closePlugin: (() => void); + on(type: T, callback: ((event: EventsMap[T]) => void), props?: { + [key: string]: unknown; + }): symbol; + off(listenerId: symbol): void; + root: null | Shape; + currentFile: null | File; + currentPage: null | Page; + viewport: Viewport; + history: HistoryContext; + library: LibraryContext; + fonts: FontsContext; + currentUser: User; + activeUsers: ActiveUser[]; + theme: Theme; + localStorage: LocalStorage; + selection: Shape[]; + shapesColors(shapes: Shape[]): (Color & ColorShapeInfo)[]; + replaceColor(shapes: Shape[], oldColor: Color, newColor: Color): void; + uploadMediaUrl(name: string, url: string): Promise; + uploadMediaData(name: string, data: Uint8Array, mimeType: string): Promise; + group(shapes: Shape[]): null | Group; + ungroup(group: Group, ...other: Group[]): void; + createRectangle(): Rectangle; + createBoard(): Board; + createEllipse(): Ellipse; + createPath(): Path; + createBoolean(boolType: BooleanType, shapes: Shape[]): null | Boolean; + createShapeFromSvg(svgString: string): null | Group; + createShapeFromSvgWithImages(svgString: string): Promise; + createText(text: string): null | Text; + generateMarkup(shapes: Shape[], options?: { + type?: "html" | "svg"; + }): string; + generateStyle(shapes: Shape[], options?: { + type?: "css"; + withPrelude?: boolean; + includeChildren?: boolean; + }): string; + generateFontFaces(shapes: Shape[]): Promise; + openViewer(): void; + createPage(): Page; + openPage(page: Page, newWindow?: boolean): void; + alignHorizontal(shapes: Shape[], direction: "center" | "left" | "right"): void; + alignVertical(shapes: Shape[], direction: "center" | "top" | "bottom"): void; + distributeHorizontal(shapes: Shape[]): void; + distributeVertical(shapes: Shape[]): void; + flatten(shapes: Shape[]): Path[]; +} +``` + +Hierarchy + +* Omit + + Penpot + +Member details not provided (too long). Call this tool with a member name for more information. +``` + +**Nota:** no existe `penpot.createImage()`. La lista de miembros de arriba es exhaustiva para el objeto +`penpot`; cualquier miembro ausente de ella no debe aparecer en ningún seed. + +--- + +## Captura 3 — `penpot_api_info {"type": "Board"}` + +``` +Interface Board +=============== + +Represents a board in Penpot. +This interface extends `ShapeBase` and includes properties and methods specific to board. + +``` +interface Board { + type: "board"; + clipContent: boolean; + showInViewMode: boolean; + grid?: GridLayout; + flex?: FlexLayout; + guides: Guide[]; + rulerGuides: RulerGuide[]; + horizontalSizing?: "auto" | "fix"; + verticalSizing?: "auto" | "fix"; + fills: Fill[]; + children: Shape[]; + appendChild(child: Shape): void; + insertChild(index: number, child: Shape): void; + addFlexLayout(): FlexLayout; + addGridLayout(): GridLayout; + addRulerGuide(orientation: RulerGuideOrientation, value: number): RulerGuide; + removeRulerGuide(guide: RulerGuide): void; + isVariantContainer(): boolean; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; +} +``` + +Hierarchy (view full) + +* ShapeBase + + Board + - VariantContainer + +Referenced by: CloseOverlay, CommentThread, Context, ContextTypesUtils, Flow, NavigateTo, OpenOverlay, OverlayAction, Page, Penpot, RulerGuide, Shape, ToggleOverlay + +Member details not provided (too long). Call this tool with a member name for more information. +``` + +**Nota:** `grid?: GridLayout` y `flex?: FlexLayout`. **No hay ninguna propiedad `layout`.** Este bloque +es la refutación documental directa del error #2 del diagnóstico. + +--- + +## Captura 4 — `penpot_api_info {"type": "Text"}` + +``` +Interface Text +============== + +Text represents a text element in the Penpot application, extending the base shape interface. +It includes various properties to define the text content and its styling attributes. + +``` +interface Text { + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + fills: Fill[] | "mixed"; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + type: "text"; + characters: string; + growType: "fixed" | "auto-width" | "auto-height"; + fontId: string; + fontFamily: string; + fontVariantId: string; + fontSize: string; + fontWeight: string; + fontStyle: + | null + | "normal" + | "italic" + | "mixed"; + lineHeight: string; + letterSpacing: string; + textTransform: + | null + | "mixed" + | "uppercase" + | "capitalize" + | "lowercase"; + textDecoration: + | null + | "mixed" + | "underline" + | "line-through"; + direction: + | null + | "mixed" + | "ltr" + | "rtl"; + align: + | null + | "center" + | "left" + | "right" + | "mixed" + | "justify"; + verticalAlign: + | null + | "center" + | "top" + | "bottom"; + getRange(start: number, end: number): TextRange; + applyTypography(typography: LibraryTypography): void; +} +``` + +Hierarchy (view full) + +* ShapeBase + + Text + +Referenced by: Context, ContextTypesUtils, Font, Penpot, Shape, TextRange + +Member details not provided (too long). Call this tool with a member name for more information. +``` + +**Notas:** `fontSize`, `fontWeight`, `lineHeight` y `letterSpacing` son **`string`**, no `number`. La +propiedad de alineación horizontal es **`align`**; **no existe `textAlign`** ni `color` (el color de un +texto va en `fills`). El objeto `Text` no es extensible: asignar una propiedad ausente de esta lista +**lanza excepción** y aborta todo el `execute_code`. + +--- + +## Captura 5 — `penpot_api_info {"type": "Rectangle"}` + +``` +Interface Rectangle +=================== + +Represents a rectangle shape in Penpot. +This interface extends `ShapeBase` and includes properties specific to rectangles. + +``` +interface Rectangle { + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + type: "rectangle"; + fills: Fill[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; +} +``` + +Hierarchy (view full) + +* ShapeBase + + Rectangle + +Referenced by: Context, ContextTypesUtils, Penpot, Shape +``` + +--- + +## Captura 6 — `penpot_api_info {"type": "Penpot", "member": "createText"}` + +**Este es el bloque que causa la falla de producción.** El ejemplo oficial llama +`penpot.createText()` **sin argumento**, forma que devuelve `null` en el runtime real. Se captura +verbatim precisamente para poder enseñarle al modelo, en los seeds del grupo C, a leer el **contrato** +(`createText(text)`, `text: string`, `Requires content:write permission`) y no el ejemplo. + +``` +``` +createText(text): null | Text +``` + +* Creates a Text shape with the specified text content. Requires `content:write` permission. + + Parameters + + text: string + + The text content for the Text shape. + + Returns null | Text + + Returns the new created shape, if the shape wasn't created can return null. + + Example + ``` + const board = penpot.createBoard();let text;text = penpot.createText();// just change the values like thistext.growType = 'auto-height';text.fontFamily = 'Work Sans';text.fontSize = '12';text.fills = [{fillColor: '#9f05ff', fillOpacity: 1}];text.strokes = [{strokeOpacity: 1, strokeStyle: 'solid', strokeWidth: 2, strokeColor: '#deabff', strokeAlignment: 'outer'}];board.appendChild(text); + ``` +``` + +--- + +## Captura 7 — `penpot_api_info {"type": "Penpot", "member": "createBoard"}` + +``` +``` +createBoard(): Board +``` + +* Use this method to create a board. This is the first step before anything else, the container. Requires `content:write` permission. + Then you can add a gridlayout, flexlayout or add a shape inside the board. + Just a heads-up: board is a board in Penpot UI. + + Returns Board + + Example + ``` + const board = penpot.createBoard();// to add grid layoutboard.addGridLayout();// to add flex layoutboard.addFlexLayout();// to create a shape inside the boardconst shape = penpot.createRectangle();board.appendChild(shape); + ``` +``` + +**Nota:** este ejemplo **sí** usa `board.appendChild(shape)`, y llama `board.addGridLayout()` — no +`addFlexLayout()`, como dice erróneamente la sección de Grid Layout del overview (Captura 1). + +--- + +## Captura 8 — `penpot_api_info {"type": "Penpot", "member": "generateStyle"}` + +``` +``` +generateStyle(shapes, options?): string +``` + +* Generates styles for the given shapes. Requires `content:read` permission + + Parameters + + shapes: Shape[] + + options: { + type?: "css"; + withPrelude?: boolean; + includeChildren?: boolean; + } + - Optionaltype?: "css" + - OptionalwithPrelude?: boolean + - OptionalincludeChildren?: boolean + + Returns string + + Example + ``` + const styles = context.generateStyle(shapes, { type: 'css' });console.log(styles); + ``` +``` + +--- + +## Captura 9 — `penpot_api_info {"type": "Penpot", "member": "uploadMediaUrl"}` + +``` +``` +uploadMediaUrl(name, url): Promise +``` + +* Uploads media to Penpot and retrieves its image data. Requires `content:write` permission. + + Parameters + + name: string + + The name of the media. + + url: string + + The URL of the media to be uploaded. + + Returns Promise + + Returns a promise that resolves to the image data of the uploaded media. + + Example + ``` + const imageData = await context.uploadMediaUrl('example', 'https://example.com/image.jpg');console.log(imageData);// to insert the image in a shape we can doconst board = penpot.createBoard();const shape = penpot.createRectangle();board.appendChild(shape);shape.fills = [{ fillOpacity: 1, fillImage: imageData }]; + ``` +``` + +--- + +## Captura 10 — `penpot_api_info {"type": "Penpot", "member": "createBoolean"}` + +``` +``` +createBoolean(boolType, shapes): null | Boolean +``` + +* Creates a Boolean shape based on the specified boolean operation and shapes. Requires `content:write` permission. + + Parameters + + boolType: BooleanType + + The type of boolean operation ('union', 'difference', 'exclude', 'intersection'). + + shapes: Shape[] + + An array of shapes to perform the boolean operation on. + + Returns null | Boolean + + Returns the newly created Boolean shape resulting from the boolean operation. + + Example + ``` + const booleanShape = context.createBoolean('union', [shape1, shape2]); + ``` +``` + +--- + +## Captura 11 — `penpot_api_info {"type": "Penpot", "member": "createShapeFromSvg"}` + +``` +``` +createShapeFromSvg(svgString): null | Group +``` + +* Creates a Group from an SVG string. Requires `content:write` permission. + + Parameters + + svgString: string + + The SVG string representing the shapes to be converted into a group. + + Returns null | Group + + Returns the newly created Group containing the shapes from the SVG. + + Example + ``` + const svgGroup = context.createShapeFromSvg('...'); + ``` +``` + +--- + +## Captura 12 — `penpot_api_info {"type": "Fill"}` + +``` +Interface Fill +============== + +Represents fill properties in Penpot. You can add a fill to any shape except for groups. +This interface includes properties for defining solid color fills, gradient fills, and image fills. + +``` +interface Fill { + fillColor?: string; + fillOpacity?: number; + fillColorGradient?: Gradient; + fillColorRefFile?: string; + fillColorRefId?: string; + fillImage?: ImageData; +} +``` + +Referenced by: Board, Boolean, Ellipse, Group, Image, LibraryColor, Path, Rectangle, ShapeBase, SvgRaw, Text, TextRange, VariantContainer + +## Properties + +### fillColor + +``` +fillColor?: string +``` + +The optional solid fill color, represented as a string (e.g., '#FF5733'). +### fillOpacity + +``` +fillOpacity?: number +``` + +The optional opacity level of the solid fill color, ranging from 0 (fully transparent) to 1 (fully opaque). +Defaults to 1 if omitted. +### fillColorGradient + +``` +fillColorGradient?: Gradient +``` + +The optional gradient fill defined by a Gradient object. +### fillColorRefFile + +``` +fillColorRefFile?: string +``` + +The optional reference to an external file for the fill color. +### fillColorRefId + +``` +fillColorRefId?: string +``` + +The optional reference ID within the external file for the fill color. +### fillImage + +``` +fillImage?: ImageData +``` + +The optional image fill defined by an ImageData object. +``` + +--- + +## Captura 13 — `penpot_api_info {"type": "Gradient"}` + +``` +Type Alias Gradient +=================== + +``` +Gradient: { + type: "linear" | "radial"; + startX: number; + startY: number; + endX: number; + endY: number; + width: number; + stops: { + color: string; + opacity?: number; + offset: number; + }[]; +} +``` + +Represents a gradient configuration in Penpot. +A gradient can be either linear or radial and includes properties to define its shape, position, and color stops. + +Type declaration + +* type: "linear" | "radial" + + Specifies the type of gradient. + + + 'linear': A gradient that transitions colors along a straight line. + + 'radial': A gradient that transitions colors radiating outward from a central point. + + Example + ``` + const gradient: Gradient = { type: 'linear', startX: 0, startY: 0, endX: 100, endY: 100, width: 100, stops: [{ color: '#FF5733', offset: 0 }] }; + ``` +* startX: number + + The X-coordinate of the starting point of the gradient. +* startY: number + + The Y-coordinate of the starting point of the gradient. +* endX: number + + The X-coordinate of the ending point of the gradient. +* endY: number + + The Y-coordinate of the ending point of the gradient. +* width: number + + The width of the gradient. For radial gradients, this could be interpreted as the radius. +* stops: { color: string; opacity?: number; offset: number; }[] + + An array of color stops that define the gradient. + +Referenced by: Color, Fill, LibraryColor, Stroke +``` + +--- + +## Captura 14 — `penpot_api_info {"type": "Stroke"}` + +``` +Interface Stroke +================ + +Represents stroke properties in Penpot. You can add a stroke to any shape except for groups. +This interface includes properties for defining the color, style, width, alignment, and caps of a stroke. + +``` +interface Stroke { + strokeColor?: string; + strokeColorRefFile?: string; + strokeColorRefId?: string; + strokeOpacity?: number; + strokeStyle?: + | "svg" + | "none" + | "mixed" + | "solid" + | "dotted" + | "dashed"; + strokeWidth?: number; + strokeAlignment?: "center" | "inner" | "outer"; + strokeCapStart?: StrokeCap; + strokeCapEnd?: StrokeCap; + strokeColorGradient?: Gradient; +} +``` + +Referenced by: Board, Boolean, Ellipse, Group, Image, LibraryColor, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer +``` + +--- + +## Captura 15 — `penpot_api_info {"type": "Shadow"}` + +``` +Interface Shadow +================ + +Represents shadow properties in Penpot. +This interface includes properties for defining drop shadows and inner shadows, along with their visual attributes. + +``` +interface Shadow { + id?: string; + style?: "drop-shadow" | "inner-shadow"; + offsetX?: number; + offsetY?: number; + blur?: number; + spread?: number; + hidden?: boolean; + color?: Color; +} +``` + +Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + +## Properties + +### id + +``` +id?: string +``` + +The optional unique identifier for the shadow. +### style + +``` +style?: "drop-shadow" | "inner-shadow" +``` + +The optional style of the shadow. + +* 'drop-shadow': A shadow cast outside the element. +* 'inner-shadow': A shadow cast inside the element. +### offsetX + +``` +offsetX?: number +``` + +The optional X-axis offset of the shadow. +### offsetY + +``` +offsetY?: number +``` + +The optional Y-axis offset of the shadow. +### blur + +``` +blur?: number +``` + +The optional blur radius of the shadow. +### spread + +``` +spread?: number +``` + +The optional spread radius of the shadow. +### hidden + +``` +hidden?: boolean +``` + +Specifies whether the shadow is hidden. +Defaults to false if omitted. +### color + +``` +color?: Color +``` + +The optional color of the shadow, defined by a Color object. +``` + +**Nota:** `Shadow.color` es un **`Color`** (`{ color, opacity }`), **no** un `Fill`. Escribir +`shadows = [{ ..., color: { fillColor: '#000' } }]` es el bug clásico. + +--- + +## Captura 16 — `penpot_api_info {"type": "Color"}` + +``` +Interface Color +=============== + +Represents color properties in Penpot. +This interface includes properties for defining solid colors, gradients, and image fills, along with metadata. + +``` +interface Color { + id?: string; + fileId?: string; + name?: string; + path?: string; + color?: string; + opacity?: number; + refId?: string; + refFile?: string; + gradient?: Gradient; + image?: ImageData; +} +``` + +Referenced by: Context, Penpot, Shadow + +## Properties + +### id + +``` +id?: string +``` + +The optional reference ID for an external color definition. +### fileId + +``` +fileId?: string +``` + +The optional reference to an external file for the color definition. +### name + +``` +name?: string +``` + +The optional name of the color. +### path + +``` +path?: string +``` + +The optional path or category to which this color belongs. +### color + +``` +color?: string +``` + +The optional solid color, represented as a string (e.g., '#FF5733'). +### opacity + +``` +opacity?: number +``` + +The optional opacity level of the color, ranging from 0 (fully transparent) to 1 (fully opaque). +Defaults to 1 if omitted. +### refId + +``` +refId?: string +``` + +The optional reference ID for an external color definition. + +Deprecated + +Use `id` instead +### refFile + +``` +refFile?: string +``` + +The optional reference to an external file for the color definition. + +Deprecated + +Use `fileId` +### gradient + +``` +gradient?: Gradient +``` + +The optional gradient fill defined by a Gradient object. +### image + +``` +image?: ImageData +``` + +The optional image fill defined by an ImageData object. +``` + +--- + +## Captura 17 — `penpot_api_info {"type": "FlexLayout"}` + +``` +Interface FlexLayout +==================== + +Represents a flexible layout configuration in Penpot. +This interface extends `CommonLayout` and includes properties for defining the direction, +wrapping behavior, and child management of a flex layout. + +``` +interface FlexLayout { + alignItems?: + | "center" + | "start" + | "end" + | "stretch"; + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + justifyItems?: + | "center" + | "start" + | "end" + | "stretch"; + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + rowGap: number; + columnGap: number; + verticalPadding: number; + horizontalPadding: number; + topPadding: number; + rightPadding: number; + bottomPadding: number; + leftPadding: number; + horizontalSizing: "fill" | "auto" | "fit-content"; + verticalSizing: "fill" | "auto" | "fit-content"; + remove(): void; + dir: + | "row" + | "row-reverse" + | "column" + | "column-reverse"; + wrap?: "wrap" | "nowrap"; + appendChild(child: Shape): void; +} +``` + +Hierarchy (view full) + +* CommonLayout + + FlexLayout + +Referenced by: Board, VariantContainer +``` + +**Nota:** no existe una propiedad `gap`. Solo `rowGap` y `columnGap`. `flex.appendChild` figura en el +tipo pero el overview declara explícitamente que está **roto**: usar `board.appendChild`. + +--- + +## Captura 18 — `penpot_api_info {"type": "GridLayout"}` + +``` +Interface GridLayout +==================== + +GridLayout represents a grid layout in the Penpot application, extending the common layout interface. +It includes properties and methods to manage rows, columns, and child elements within the grid. + +``` +interface GridLayout { + alignItems?: + | "center" + | "start" + | "end" + | "stretch"; + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + justifyItems?: + | "center" + | "start" + | "end" + | "stretch"; + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + rowGap: number; + columnGap: number; + verticalPadding: number; + horizontalPadding: number; + topPadding: number; + rightPadding: number; + bottomPadding: number; + leftPadding: number; + horizontalSizing: "fill" | "auto" | "fit-content"; + verticalSizing: "fill" | "auto" | "fit-content"; + remove(): void; + dir: "row" | "column"; + rows: Track[]; + columns: Track[]; + addRow(type: TrackType, value?: number): void; + addRowAtIndex(index: number, type: TrackType, value?: number): void; + addColumn(type: TrackType, value?: number): void; + addColumnAtIndex(index: number, type: TrackType, value: number): void; + removeRow(index: number): void; + removeColumn(index: number): void; + setColumn(index: number, type: TrackType, value?: number): void; + setRow(index: number, type: TrackType, value?: number): void; + appendChild(child: Shape, row: number, column: number): void; +} +``` + +Hierarchy (view full) + +* CommonLayout + + GridLayout + +Referenced by: Board, VariantContainer +``` + +**Nota:** `grid.appendChild(child, row, column)` con índices **1-based** (verificado en vivo: pasar 0 se +clampea a 1). El overview dice `board.flex.appendChild(shape, row, column)` — es un typo del overview. + +--- + +## Captura 19 — `penpot_api_info {"type": "Track"}` + +``` +Interface Track +=============== + +Represents a track configuration in Penpot. +This interface includes properties for defining the type and value of a track used in layout configurations. + +``` +interface Track { + type: TrackType; + value: null | number; +} +``` + +Referenced by: GridLayout + +## Properties + +### type + +``` +type: TrackType +``` + +The type of the track. +This can be one of the following values: + +* 'flex': A flexible track type. +* 'fixed': A fixed track type. +* 'percent': A track type defined by a percentage. +* 'auto': An automatic track type. +### value + +``` +value: null | number +``` + +The value of the track. +This can be a number representing the size of the track, or null if not applicable. +``` + +--- + +## Captura 20 — `penpot_api_info {"type": "TrackType"}` + +``` +Type Alias TrackType +==================== + +``` +TrackType: + | "flex" + | "fixed" + | "percent" + | "auto" +``` + +Represents the type of track in Penpot. +This type defines various track types that can be used in layout configurations. + +Referenced by: GridLayout, Track +``` + +--- + +## Captura 21 — `penpot_api_info {"type": "LayoutChildProperties"}` + +``` +Interface LayoutChildProperties +=============================== + +Properties for defining the layout of a child element in Penpot. + +``` +interface LayoutChildProperties { + absolute: boolean; + zIndex: number; + horizontalSizing: "fill" | "auto" | "fix"; + verticalSizing: "fill" | "auto" | "fix"; + alignSelf: + | "center" + | "auto" + | "start" + | "end" + | "stretch"; + horizontalMargin: number; + verticalMargin: number; + topMargin: number; + rightMargin: number; + bottomMargin: number; + leftMargin: number; + maxWidth: null | number; + maxHeight: null | number; + minWidth: null | number; + minHeight: null | number; +} +``` + +Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer +``` + +**Nota:** los valores de `layoutChild.horizontalSizing` son `"fill" | "auto" | "fix"` — **`fix`**, no +`fixed`, y **no** `fit-content` (ese es el vocabulario de `FlexLayout`, que aplica al board contenedor). +`zIndex` figura en el tipo pero **se ignora silenciosamente** en el runtime. + +--- + +## Captura 22 — `penpot_api_info {"type": "LayoutCellProperties"}` + +``` +Interface LayoutCellProperties +============================== + +Properties for defining the layout of a cell in Penpot. + +``` +interface LayoutCellProperties { + row?: number; + rowSpan?: number; + column?: number; + columnSpan?: number; + areaName?: string; + position?: "area" | "auto" | "manual"; +} +``` + +## Properties + +### row + +``` +row?: number +``` + +The row index of the cell. +This value is optional and indicates the starting row of the cell. +### rowSpan + +``` +rowSpan?: number +``` + +The number of rows the cell should span. +This value is optional and determines the vertical span of the cell. +### column + +``` +column?: number +``` + +The column index of the cell. +This value is optional and indicates the starting column of the cell. +### columnSpan + +``` +columnSpan?: number +``` + +The number of columns the cell should span. +This value is optional and determines the horizontal span of the cell. +### areaName + +``` +areaName?: string +``` + +The name of the grid area that this cell belongs to. +This value is optional and can be used to define named grid areas. +### position + +``` +position?: "area" | "auto" | "manual" +``` + +The positioning mode of the cell. +This value can be 'auto', 'manual', or 'area' and determines how the cell is positioned within the layout. +``` + +--- + +## Captura 23 — `penpot_api_info {"type": "Page"}` + +``` +Interface Page +============== + +Page represents a page in the Penpot application. +It includes properties for the page's identifier and name, as well as methods for managing shapes on the page. + +``` +interface Page { + id: string; + name: string; + rulerGuides: RulerGuide[]; + root: Shape; + getShapeById(id: string): null | Shape; + findShapes(criteria?: { + name?: string; + nameLike?: string; + type?: + | "boolean" + | "path" + | "ellipse" + | "image" + | "text" + | "group" + | "board" + | "rectangle" + | "svg-raw"; + }): Shape[]; + flows: Flow[]; + createFlow(name: string, board: Board): Flow; + removeFlow(flow: Flow): void; + addRulerGuide(orientation: RulerGuideOrientation, value: number, board?: Board): RulerGuide; + removeRulerGuide(guide: RulerGuide): void; + addCommentThread(content: string, position: Point): Promise; + removeCommentThread(commentThread: CommentThread): Promise; + findCommentThreads(criteria?: { + onlyYours: boolean; + showResolved: boolean; + }): Promise; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; +} +``` + +Hierarchy (view full) + +* PluginData + + Page + +Referenced by: Context, EventsMap, File, Flow, Penpot +``` + +**Nota:** `page.getShapeById(id)` toma **un** argumento y es el método de `Page`. +`penpotUtils.findShapeById(id)` también toma **un** argumento y busca globalmente. No existe ninguna +firma de dos argumentos. + +--- + +## Captura 24 — `penpot_api_info {"type": "Library"}` + +``` +Interface Library +================= + +Represents a library in Penpot, containing colors, typographies, and components. + +``` +interface Library { + id: string; + name: string; + colors: LibraryColor[]; + typographies: LibraryTypography[]; + components: LibraryComponent[]; + createColor(): LibraryColor; + createTypography(): LibraryTypography; + createComponent(shapes: Shape[]): LibraryComponent; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; +} +``` + +Hierarchy (view full) + +* PluginData + + Library + +Referenced by: LibraryContext +``` + +--- + +## Captura 25 — `penpot_api_info {"type": "LibraryColor"}` + +``` +Interface LibraryColor +====================== + +Represents a color element from a library in Penpot. +This interface extends `LibraryElement` and includes properties specific to color elements. + +``` +interface LibraryColor { + color?: string; + opacity?: number; + gradient?: Gradient; + image?: ImageData; + asFill(): Fill; + asStroke(): Stroke; + id: string; + libraryId: string; + name: string; + path: string; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; +} +``` + +Hierarchy (view full) + +* LibraryElement + + LibraryColor + +Referenced by: Library +``` + +--- + +## Captura 26 — `penpot_api_info {"type": "LibraryTypography"}` + +``` +Interface LibraryTypography +=========================== + +Represents a typography element from a library in Penpot. +This interface extends `LibraryElement` and includes properties specific to typography elements. + +``` +interface LibraryTypography { + id: string; + libraryId: string; + name: string; + path: string; + fontId: string; + fontFamily: string; + fontVariantId: string; + fontSize: string; + fontWeight: string; + fontStyle?: null | "normal" | "italic"; + lineHeight: string; + letterSpacing: string; + textTransform?: + | null + | "uppercase" + | "capitalize" + | "lowercase"; + applyToText(shape: Shape): void; + applyToTextRange(range: TextRange): void; + setFont(font: Font, variant?: FontVariant): void; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; +} +``` + +Hierarchy (view full) + +* LibraryElement + + LibraryTypography + +Referenced by: Library, Text, TextRange +``` + +**Nota crítica:** `setFont` **está en el tipo pero NO existe en el runtime de esta versión** +(`t.setFont is not a function`, ver `penpot_errors.md`). Setear las propiedades de tipografía una por +una. Este es el ejemplo canónico de "la documentación no es el runtime" para los seeds del grupo C. + +--- + +## Captura 27 — `penpot_api_info {"type": "LibraryComponent"}` + +``` +Interface LibraryComponent +========================== + +Represents a component element from a library in Penpot. +This interface extends `LibraryElement` and includes properties specific to component elements. + +``` +interface LibraryComponent { + instance(): Shape; + mainInstance(): Shape; + isVariant(): boolean; + transformInVariant(): void; + id: string; + libraryId: string; + name: string; + path: string; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; +} +``` + +Hierarchy (view full) + +* LibraryElement + + LibraryComponent + - LibraryVariantComponent + +Referenced by: Board, Boolean, ContextTypesUtils, Ellipse, Group, Image, Library, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer, Variants +``` + +--- + +## Captura 28 — `penpot_api_info {"type": "ImageData"}` + +``` +Type Alias ImageData +==================== + +``` +ImageData: { + name?: string; + width: number; + height: number; + mtype?: string; + id: string; + keepAspectRatio?: boolean; + data(): Promise; +} +``` + +Represents image data in Penpot. +This includes properties for defining the image's dimensions, metadata, and aspect ratio handling. + +Type declaration + +* Optionalname?: string + + The optional name of the image. +* width: number + + The width of the image. +* height: number + + The height of the image. +* Optionalmtype?: string + + The optional media type of the image (e.g., 'image/png', 'image/jpeg'). +* id: string + + The unique identifier for the image. +* OptionalkeepAspectRatio?: boolean + + Whether to keep the aspect ratio of the image when resizing. + Defaults to false if omitted. +* data:function + + ``` + data(): Promise + ``` + + + Returns the imaged data as a byte array. + + Returns Promise + +Referenced by: Color, Context, Fill, LibraryColor, Penpot +``` + +--- + +## Captura 29 — `penpot_api_info {"type": "Export"}` + +``` +Interface Export +================ + +Represents export settings in Penpot. +This interface includes properties for defining export configurations. + +``` +interface Export { + type: + | "svg" + | "png" + | "jpeg" + | "pdf"; + scale?: number; + suffix?: string; + skipChildren?: boolean; +} +``` + +Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + +## Properties + +### type + +``` +type: + | "svg" + | "png" + | "jpeg" + | "pdf" +``` + +Type of the file to export. Can be one of the following values: png, jpeg, svg, pdf +### scale + +``` +scale?: number +``` + +For bitmap formats represent the scale of the original size to resize the export +### suffix + +``` +suffix?: string +``` + +Suffix that will be appended to the resulting exported file +### skipChildren + +``` +skipChildren?: boolean +``` + +If true will ignore the children when exporting the shape +``` + +**Nota:** `Export.scale` es una propiedad del **método `shape.export(config)` de la API de plugin**, no +de la **herramienta MCP `export_shape`**. El schema de `export_shape` (ver `data/schemas/penpot.json`) +acepta únicamente `shapeId`, `format` y `mode`. Pedir `scale` o `filePath` a `export_shape` es +invención de parámetros, y es una de las sondas del holdout. + +--- + +## Captura 30 — `penpot_api_info {"type": "Group"}` + +``` +Interface Group +=============== + +Represents a group of shapes in Penpot. +This interface extends `ShapeBase` and includes properties and methods specific to groups. + +``` +interface Group { + type: "group"; + children: Shape[]; + appendChild(child: Shape): void; + insertChild(index: number, child: Shape): void; + isMask(): boolean; + makeMask(): void; + removeMask(): void; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + fills: Fill[] | "mixed"; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; +} +``` + +Hierarchy (view full) + +* ShapeBase + + Group + +Referenced by: Context, ContextTypesUtils, Penpot, Shape + +Member details not provided (too long). Call this tool with a member name for more information. +``` + +--- + +## Captura 31 — `penpot_api_info {"type": "BooleanType"}` + +``` +Type Alias BooleanType +====================== + +``` +BooleanType: + | "union" + | "difference" + | "exclude" + | "intersection" +``` + +Represents the boolean operation types available in Penpot. +These types define how shapes can be combined or modified using boolean operations. + +Referenced by: Context, Penpot +``` + +--- + +## Captura 32 — `penpot_api_info {"type": "FontsContext"}` + +``` +Interface FontsContext +====================== + +Represents the context for managing fonts in Penpot. +This interface provides methods to interact with fonts, such as retrieving fonts by ID or name. + +``` +interface FontsContext { + all: Font[]; + findById(id: string): null | Font; + findByName(name: string): null | Font; + findAllById(id: string): Font[]; + findAllByName(name: string): Font[]; +} +``` + +Referenced by: Context, Penpot + +## Properties + +### all + +``` +all: Font[] +``` + +An array containing all available fonts. + +## Methods + +### findById + +``` +findById(id): null | Font +``` + +* Finds a font by its unique identifier. + + Parameters + + id: string + + The ID of the font to find. + + Returns null | Font + + Returns the `Font` object if found, otherwise `null`. + + Example + ``` + const font = fontsContext.findById('font-id');if (font) { console.log(font.name);} + ``` +### findByName + +``` +findByName(name): null | Font +``` + +* Finds a font by its name. + + Parameters + + name: string + + The name of the font to find. + + Returns null | Font + + Returns the `Font` object if found, otherwise `null`. + + Example + ``` + const font = fontsContext.findByName('font-name');if (font) { console.log(font.name);} + ``` +### findAllById + +``` +findAllById(id): Font[] +``` + +* Finds all fonts matching a specific ID. + + Parameters + + id: string + + The ID to match against. + + Returns Font[] + + Returns an array of `Font` objects matching the provided ID. + + Example + ``` + const fonts = fontsContext.findAllById('font-id');console.log(fonts); + ``` +### findAllByName + +``` +findAllByName(name): Font[] +``` + +* Finds all fonts matching a specific name. + + Parameters + + name: string + + The name to match against. + + Returns Font[] + + Returns an array of `Font` objects matching the provided name. + + Example + ``` + const fonts = fontsContext.findAllByName('font-name');console.log(fonts); + ``` +``` + +--- + +## Captura 33 — `penpot_api_info {"type": "Font"}` + +``` +Interface Font +============== + +Represents a font in Penpot, which includes details about the font family, variants, and styling options. +This interface provides properties and methods for describing and applying fonts within Penpot. + +``` +interface Font { + name: string; + fontId: string; + fontFamily: string; + fontStyle?: null | "normal" | "italic"; + fontVariantId: string; + fontWeight: string; + variants: FontVariant[]; + applyToText(text: Text, variant?: FontVariant): void; + applyToRange(range: TextRange, variant?: FontVariant): void; +} +``` + +Referenced by: FontsContext, LibraryTypography + +Member details not provided (too long). Call this tool with a member name for more information. +``` + +**Nota:** `font.applyToText(text)` funciona pero deja `fontId` como un objeto UUID de ClojureScript +filtrado. Asignar `text.fontFamily = 'Work Sans'` como string directo bindea correctamente +(`fontId: "gfont-work-sans"`) y es la forma preferida. + +--- + +## Captura 34 — `penpot_api_info {"type": "ContextUtils"}` + +``` +Interface ContextUtils +====================== + +Utility methods for various operations in Penpot. + +``` +interface ContextUtils { + geometry: ContextGeometryUtils; + types: ContextTypesUtils; +} +``` + +Referenced by: Penpot + +## Properties + +### geometry + +``` +readonly geometry: ContextGeometryUtils +``` + +Geometry utility methods for Penpot. +Provides methods for geometric calculations, such as finding the center of a group of shapes. + +Example +``` +const centerPoint = penpot.utils.geometry.center(shapes);console.log(centerPoint); +``` +### types + +``` +readonly types: ContextTypesUtils +``` + +Type utility methods for Penpot. +Provides methods for determining the types of various shapes in Penpot. + +Example +``` +const isBoard = utils.types.isBoard(shape);console.log(isBoard); +``` +``` + +**Nota:** `penpot.utils` (`ContextUtils`) es **distinto** de `penpotUtils` (el objeto global que +inyecta el servidor MCP y que está documentado en la Captura 1). No confundirlos. diff --git a/data/schemas/penpot_errors.md b/data/schemas/penpot_errors.md new file mode 100644 index 0000000..b65e61b --- /dev/null +++ b/data/schemas/penpot_errors.md @@ -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 , 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 '')` y +`Cannot set properties of null (setting '')`. + +**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 | diff --git a/data/schemas/penpot_system_prompt.md b/data/schemas/penpot_system_prompt.md new file mode 100644 index 0000000..7bbfce1 --- /dev/null +++ b/data/schemas/penpot_system_prompt.md @@ -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".