From d9629c44ae27c4133f6446c78252ac913d63652e Mon Sep 17 00:00:00 2001 From: Alejandro Lembke Barrientos Date: Thu, 30 Jul 2026 16:46:33 +0000 Subject: [PATCH] Phase 6.1: capture verified Penpot API ground truth for the LoRA #2 dataset The 41 existing Penpot seeds contain hand-fabricated penpot_api_info and high_level_overview tool results that assert facts the server never said, which is how the model learned an API that does not exist. This adds four schema files that make the seed corpus mechanically verifiable against the real server instead. - penpot_api_docs.md: 34 verbatim captures of high_level_overview and penpot_api_info, each headed by the exact request that produced it. Every penpot_api_info tool result in a seed must be a subset of lines of this file, in original order. Records three places where the served docs contradict the runtime (addFlexLayout/addGridLayout copy-paste in the Grid section, flex.appendChild for grid children, withChildren vs includeChildren), plus the createText() example that is the direct cause of the production failure. - penpot_system_prompt.md: the server's instructions block verbatim. Goes as a system message into ~30% of the new seeds; it is the countermeasure to the "don't pick your own colours" rule that produces the grey boxes. - penpot_errors.md: the real error strings, including a section on silent failures that raise nothing at all and are why the read-back invariant exists. - PENPOT_API_VERIFIED.md: the allow-list. No seed may reference a member absent from it. Documents the four root causes (findShapeById arity 1, no shape.layout, createText() returning null, the #B1B2B5 default fill), the twelve anti-grey-box invariants, and the forbidden-pattern list the linter checks. Live re-verification of the error strings is still pending: the Penpot plugin is not currently connected, so it is deferred to the gate 5 baseline step, which needs the live connection anyway. --- data/schemas/PENPOT_API_VERIFIED.md | 416 +++++ data/schemas/penpot_api_docs.md | 2424 ++++++++++++++++++++++++++ data/schemas/penpot_errors.md | 154 ++ data/schemas/penpot_system_prompt.md | 60 + 4 files changed, 3054 insertions(+) create mode 100644 data/schemas/PENPOT_API_VERIFIED.md create mode 100644 data/schemas/penpot_api_docs.md create mode 100644 data/schemas/penpot_errors.md create mode 100644 data/schemas/penpot_system_prompt.md 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".