Phase 6: train a second LoRA for real Penpot UI design capability #5
@@ -0,0 +1,416 @@
|
|||||||
|
# PENPOT_API_VERIFIED — allow-list de la API de Penpot
|
||||||
|
|
||||||
|
**Este archivo es el contrato.** Ningún seed de `data/raw/seeds/penpot.jsonl` puede usar un miembro de
|
||||||
|
la API de Penpot que no esté listado acá. `scripts/07_lint_penpot_code.py` lo verifica con hard-fail
|
||||||
|
sobre cada payload de `code`.
|
||||||
|
|
||||||
|
Si un seed necesita un miembro ausente de esta lista: **primero** se verifica contra `penpot_api_info`,
|
||||||
|
se agrega la captura verbatim a `penpot_api_docs.md`, y **después** se agrega acá con su evidencia.
|
||||||
|
Nunca al revés.
|
||||||
|
|
||||||
|
**Dos fuentes de verdad, y no coinciden.** La columna "documentado" es lo que dice el servidor
|
||||||
|
(`penpot_api_docs.md`); la columna "verificado" es lo que hace el runtime. Cuando difieren, **manda el
|
||||||
|
runtime**, y la discrepancia se anota explícitamente porque es material de entrenamiento: los seeds del
|
||||||
|
grupo C enseñan justamente a no confiar ciegamente en la documentación.
|
||||||
|
|
||||||
|
**Procedencia de las verificaciones en vivo:** sondeo de la sesión madre contra el archivo real del
|
||||||
|
usuario el 2026-07-30, creando shapes de prueba que se borraron al terminar (el archivo quedó en su
|
||||||
|
estado original: 0 shapes, 0 colores/tipografías/componentes en la biblioteca local, verificado con
|
||||||
|
`penpotUtils.shapeStructure(penpot.root, 2)`). Registrado en `PLAN.md` § "La API verificada" y en la
|
||||||
|
página de Docmost de la Fase 6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Herramientas MCP disponibles — lista cerrada
|
||||||
|
|
||||||
|
Estas cuatro y **ninguna más**. Copiadas de `data/schemas/penpot.json`, que es byte a byte lo que el
|
||||||
|
modelo ve en inferencia.
|
||||||
|
|
||||||
|
| Herramienta | Parámetros | Requeridos |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `execute_code` | `code` | `code` |
|
||||||
|
| `export_shape` | `shapeId`, `format` (`svg`\|`png`, default `png`), `mode` (`shape`\|`fill`, default `shape`) | `shapeId` |
|
||||||
|
| `high_level_overview` | (ninguno) | — |
|
||||||
|
| `penpot_api_info` | `type`, `member` | `type` |
|
||||||
|
|
||||||
|
**Prohibido y sondeado por el holdout:**
|
||||||
|
|
||||||
|
- `import_image` — **no existe en este deployment**. El overview la menciona en prosa; la lista de
|
||||||
|
herramientas es autoritativa. Ruta correcta: `await penpot.uploadMediaUrl(name, url)`.
|
||||||
|
- `export_shape` con `filePath` — el parámetro está eliminado del schema en este deployment
|
||||||
|
(ver `PENPOT_DEPLOYMENT_NOTES.md`). Inventarlo es invención de parámetros.
|
||||||
|
- `export_shape` con `scale` — `scale` es propiedad del método de plugin `shape.export(config)`
|
||||||
|
(`penpot_api_docs.md` Captura 29), **no** de la herramienta MCP. No hay forma de pedir 2x.
|
||||||
|
- Llamar `high_level_overview` dos veces — su propia descripción lo prohíbe
|
||||||
|
(`If you have already read the 'Penpot High-Level Overview', you must not call this tool.`).
|
||||||
|
Por eso aparece en exactamente **2** de los 96 seeds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. El objeto `penpot` — miembros permitidos
|
||||||
|
|
||||||
|
Lista cerrada, de `penpot_api_docs.md` Captura 2.
|
||||||
|
|
||||||
|
**Creación:** `createBoard()`, `createRectangle()`, `createEllipse()`, `createPath()`,
|
||||||
|
`createText(text)`, `createBoolean(boolType, shapes)`, `createShapeFromSvg(svgString)`,
|
||||||
|
`createShapeFromSvgWithImages(svgString)`, `createPage()`.
|
||||||
|
|
||||||
|
**Medios:** `uploadMediaUrl(name, url)`, `uploadMediaData(name, data, mimeType)`.
|
||||||
|
|
||||||
|
**Estructura y navegación:** `root`, `currentPage`, `currentFile`, `selection`, `openPage(page)`,
|
||||||
|
`group(shapes)`, `ungroup(group, ...other)`, `flatten(shapes)`.
|
||||||
|
|
||||||
|
**Alineación:** `alignHorizontal(shapes, dir)`, `alignVertical(shapes, dir)`,
|
||||||
|
`distributeHorizontal(shapes)`, `distributeVertical(shapes)`.
|
||||||
|
|
||||||
|
**Generación:** `generateMarkup(shapes, {type})`, `generateStyle(shapes, {type, withPrelude, includeChildren})`,
|
||||||
|
`generateFontFaces(shapes)`.
|
||||||
|
|
||||||
|
**Color:** `shapesColors(shapes)`, `replaceColor(shapes, oldColor, newColor)`.
|
||||||
|
|
||||||
|
**Contextos:** `library`, `fonts`, `history`, `viewport`, `theme`, `localStorage`, `utils`,
|
||||||
|
`currentUser`, `activeUsers`.
|
||||||
|
|
||||||
|
### PROHIBIDO — no existe
|
||||||
|
|
||||||
|
| Miembro inventado | Realidad |
|
||||||
|
| --- | --- |
|
||||||
|
| `penpot.createImage()` | No existe. Las imágenes van como `fills = [{fillOpacity: 1, fillImage: imageData}]` |
|
||||||
|
| `penpot.createComponent()` | Es `penpot.library.local.createComponent(shapes)` |
|
||||||
|
| `penpot.findShapeById()` | Es `penpotUtils.findShapeById(id)` o `page.getShapeById(id)` |
|
||||||
|
| cualquier miembro ausente de la Captura 2 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `penpotUtils` — el objeto que inyecta el servidor MCP
|
||||||
|
|
||||||
|
Lista cerrada, de `penpot_api_docs.md` Captura 1. **Distinto de `penpot.utils`** (`ContextUtils`,
|
||||||
|
Captura 34), que solo tiene `geometry` y `types`.
|
||||||
|
|
||||||
|
| Firma | Aridad | Nota |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `getPages()` | 0 | `{id, name}[]` |
|
||||||
|
| `getPageById(id)` | 1 | |
|
||||||
|
| `getPageByName(name)` | 1 | |
|
||||||
|
| `shapeStructure(shape, maxDepth?)` | 1-2 | Devuelve `{id, name, type, children?, layout?}` |
|
||||||
|
| **`findShapeById(id)`** | **1** | **Ver § 3.1. La forma de 2 argumentos es el bug #1 del diagnóstico** |
|
||||||
|
| `findShape(predicate, root?)` | 1-2 | Sin `root` busca en todas las páginas |
|
||||||
|
| `findShapes(predicate, root?)` | 1-2 | |
|
||||||
|
| `isContainedIn(shape, container)` | 2 | |
|
||||||
|
| `setParentXY(shape, parentX, parentY)` | 3 | `parentX`/`parentY` son read-only, esta es la única vía |
|
||||||
|
| `analyzeDescendants(root, evaluator, maxDepth?)` | 2-3 | Devuelve `{shape, result}[]` |
|
||||||
|
|
||||||
|
### PROHIBIDO
|
||||||
|
|
||||||
|
`penpotUtils.importImage(...)` — no existe (`penpotUtils.importImage is not a function`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Los cuatro errores que esta fase corrige
|
||||||
|
|
||||||
|
### 3.1 `findShapeById` tiene aridad 1
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **Documentado** | `findShapeById(id: string): Shape \| null` (Captura 1) |
|
||||||
|
| **Verificado** | `penpotUtils.findShapeById.length === 1`. `findShapeById(id)` → `{found: true}`. `findShapeById(page, id)` → **`{found: false}`, sin lanzar** |
|
||||||
|
| **Correcto** | `const s = penpotUtils.findShapeById(id); if (!s) { ... }` |
|
||||||
|
| **PROHIBIDO** | `penpotUtils.findShapeById(page, id)` — **27 ocurrencias sobre 21 de los 41 seeds viejos** |
|
||||||
|
|
||||||
|
El fallo es **silencioso**: devuelve `null` y revienta en la línea siguiente con
|
||||||
|
`Cannot read properties of null (reading '<prop>')`. El stack apunta al síntoma, no a la causa.
|
||||||
|
|
||||||
|
**Familia de búsqueda completa y permitida:**
|
||||||
|
|
||||||
|
| Forma | Cuándo |
|
||||||
|
| --- | --- |
|
||||||
|
| `penpotUtils.findShapeById(id)` | Id conocido, búsqueda global |
|
||||||
|
| `page.getShapeById(id)` | Id conocido, dentro de una página concreta |
|
||||||
|
| `penpotUtils.findShape(pred, root?)` | Primer match por predicado |
|
||||||
|
| `penpotUtils.findShapes(pred, root?)` | Todos los matches por predicado |
|
||||||
|
| `page.findShapes({name, nameLike, type})` | Búsqueda por criterio declarativo (Captura 23) |
|
||||||
|
| `penpotUtils.getPageByName(name)` | Obtener la página primero |
|
||||||
|
|
||||||
|
### 3.2 `shape.layout` no existe
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **Documentado** | `Board` declara `grid?: GridLayout` y `flex?: FlexLayout`. **No hay `layout`** (Captura 3) |
|
||||||
|
| **Verificado** | `'layout' in shape === false`; `shape.layout === undefined`. `board.flex` y `board.grid` devuelven **`null`** (no `undefined`) cuando no hay layout |
|
||||||
|
| **Correcto** | `if (board.flex) { board.flex.dir = 'column'; }` |
|
||||||
|
| **PROHIBIDO** | `shape.layout`, `board.layout`, `!!form.layout` — 8 ocurrencias sobre 5 seeds viejos |
|
||||||
|
|
||||||
|
**Origen del error, y por qué hay un seed dedicado a desambiguarlo:** la salida de
|
||||||
|
`penpotUtils.shapeStructure()` **sí** trae una clave `layout` (`{id, name, type, children?, layout?}`).
|
||||||
|
Es una clave del **output del helper**, no una propiedad del shape. Confundirlas es exactamente el error
|
||||||
|
que hay que desaprender.
|
||||||
|
|
||||||
|
### 3.3 `createText()` sin argumento devuelve `null`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **Documentado** | Contrato: `createText(text: string): null \| Text`. **Ejemplo oficial: `text = penpot.createText();` sin argumento** (Captura 6) |
|
||||||
|
| **Verificado** | `createText('Hola mundo')` → `Text`. **`createText()` → `null`. `createText('')` → `null`** |
|
||||||
|
| **Correcto** | `const t = penpot.createText('Margherita'); if (!t) throw new Error('createText devolvió null');` |
|
||||||
|
| **PROHIBIDO** | `penpot.createText()` y `penpot.createText('')` |
|
||||||
|
|
||||||
|
Un modelo que consulta la documentación y sigue el ejemplo obtiene `null`, no puede crear texto, y
|
||||||
|
abandona: quedan solo rectángulos. Ésa es la mitad del síntoma reportado.
|
||||||
|
|
||||||
|
### 3.4 El gris `#B1B2B5`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **Verificado** | El fill por defecto de `penpot.createRectangle()` es **`#B1B2B5`**. El de `createBoard()` es blanco |
|
||||||
|
| **Correcto** | Invariante R1: todo shape creado recibe `fills` explícito **antes** de insertarse |
|
||||||
|
|
||||||
|
El único ejemplo de flex layout en los 41 seeds viejos crea un board de 400×60 con dos rectángulos a los
|
||||||
|
que **nunca** asigna `fills` — el ejemplo canónico de "armar un layout" en los datos de entrenamiento
|
||||||
|
produce exactamente dos cajas grises.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Texto
|
||||||
|
|
||||||
|
| Miembro | Estado | Detalle verificado |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `characters` | ✅ | El contenido renderizado |
|
||||||
|
| `fontSize` | ✅ **string** | `'48'`. Los números se coercionan, pero el lint exige el string |
|
||||||
|
| `fontWeight` | ✅ **string** | `'700'` |
|
||||||
|
| `lineHeight` | ✅ **string** | `'1.2'` |
|
||||||
|
| `letterSpacing` | ✅ **string** | `'-1'` (px). **`'-0.02'` se lee de vuelta como `"0"`: los negativos en em se ignoran en silencio** |
|
||||||
|
| `fontFamily` | ✅ | `text.fontFamily = 'Work Sans'` como string directo **bindea** (`fontId: "gfont-work-sans"`). 1914 fuentes disponibles |
|
||||||
|
| `growType` | ✅ | `'auto-width'` \| `'auto-height'` \| `'fixed'`. Default `'auto-width'` |
|
||||||
|
| `align` | ✅ | `'left'` \| `'center'` \| `'right'` \| `'justify'` |
|
||||||
|
| `verticalAlign` | ✅ | `'top'` \| `'center'` \| `'bottom'` |
|
||||||
|
| `fills` | ✅ | El color del texto va acá. Default `[{fillColor:"#000000", fillOpacity:1}]` |
|
||||||
|
| `textTransform`, `textDecoration`, `direction` | ✅ | Captura 4 |
|
||||||
|
| `getRange(start, end)` | ✅ | Devuelve `TextRange` |
|
||||||
|
| `applyTypography(typography)` | ✅ | |
|
||||||
|
| **`textAlign`** | ❌ **LANZA** | `Cannot add property textAlign, object is not extensible`. **Mata todo el `execute_code`.** Usar `align` |
|
||||||
|
| **`color`** | ❌ **LANZA** | El objeto no es extensible. Usar `fills` |
|
||||||
|
| `font.applyToText(text)` | ⚠️ | Funciona, pero deja `fontId` como un objeto UUID de ClojureScript filtrado. **Preferir `fontFamily` como string** |
|
||||||
|
|
||||||
|
**Defaults de un `Text` recién creado, verificados:** `fontSize:"14"`, `fontFamily:"sourcesanspro"`,
|
||||||
|
`fontWeight:"400"`, `fills:[{fillColor:"#000000",fillOpacity:1}]`, `growType:"auto-width"`,
|
||||||
|
`width/height = [1, 1]`.
|
||||||
|
|
||||||
|
**Dimensionado (invariantes R4/R5/R6):**
|
||||||
|
|
||||||
|
- El auto-sizing **no es inmediato**: dormir ~120 ms antes de leer el bounding box.
|
||||||
|
- `resize(w, h)` setea `growType` a `'fixed'` **en silencio**. Restaurarlo si se quiere auto-sizing.
|
||||||
|
- `width` y `height` son **read-only**; `resize()` es la única vía.
|
||||||
|
- `parentX`/`parentY`/`boardX`/`boardY`/`bounds` son **read-only**; usar `penpotUtils.setParentXY()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Fills, gradientes, strokes, sombras, radios
|
||||||
|
|
||||||
|
| Miembro | Estado | Detalle |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `fills = [{fillColor, fillOpacity}]` | ✅ | |
|
||||||
|
| `fills` apilados | ✅ | Foto + scrim oscuro encima funciona |
|
||||||
|
| `fillColorGradient` | ✅ | Coords **normalizadas 0..1** con `width: 1`, aceptado verbatim |
|
||||||
|
| `fillImage` | ✅ | `fills = [{fillOpacity: 1, fillImage: img}]`; `img.keepAspectRatio = true` para fotos |
|
||||||
|
| `fillColorRefFile` / `fillColorRefId` | ✅ | Los pone `libraryColor.asFill()` |
|
||||||
|
| `strokes = [{strokeColor, strokeWidth, strokeStyle, strokeAlignment, strokeOpacity}]` | ✅ | |
|
||||||
|
| `borderRadius`, `borderRadiusTopLeft`… | ✅ | En rectángulo y board |
|
||||||
|
| `shadows = [{style, offsetX, offsetY, blur, spread, color}]` | ✅ | |
|
||||||
|
| **`Shadow.color`** | ⚠️ | Es un **`Color`** (`{color: '#000000', opacity: 0.12}`), **no un `Fill`**. `{fillColor: ...}` es el bug clásico |
|
||||||
|
| `blur` | ✅ | |
|
||||||
|
| `opacity`, `blendMode` | ✅ | |
|
||||||
|
|
||||||
|
**Gradiente, forma verificada:**
|
||||||
|
|
||||||
|
```js
|
||||||
|
fills = [{ fillOpacity: 1, fillColorGradient: {
|
||||||
|
type: 'linear', startX: 0, startY: 0, endX: 0, endY: 1, width: 1,
|
||||||
|
stops: [{ color: '#D62828', offset: 0 }, { color: '#F77F00', offset: 1 }]
|
||||||
|
}}]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Jerarquía e inserción
|
||||||
|
|
||||||
|
| Situación | Forma correcta |
|
||||||
|
| --- | --- |
|
||||||
|
| Padre **sin** layout | `parent.insertChild(parent.children.length, shape)` |
|
||||||
|
| Board **con** flex | `board.appendChild(shape)`, llamado **en orden visual** |
|
||||||
|
| Board **con** grid | `board.grid.appendChild(shape, row, column)` — **1-based** |
|
||||||
|
| Índice específico en flex | `board.insertChild(index, shape)`, recordando el orden invertido |
|
||||||
|
| Reparentar | `newParent.appendChild(shape)` / `insertChild(...)` — quita del padre viejo, preserva x/y absolutos |
|
||||||
|
| **PROHIBIDO** | `board.flex.appendChild(shape)` — **está roto** |
|
||||||
|
|
||||||
|
**Orden invertido, verificado.** En `dir: 'column'` (y `'row'`), apendeando PRIMERO y luego SEGUNDO, el
|
||||||
|
array `children` queda `[SEGUNDO, PRIMERO, ...]`: `board.appendChild` inserta **al frente**. Por eso hay
|
||||||
|
que llamarlo en orden visual.
|
||||||
|
|
||||||
|
**La regla absoluta "nunca `appendChild`" que enseñan los seeds viejos es INCORRECTA.** El overview la
|
||||||
|
califica él mismo (`except in flex layout boards`) y la sección de Flex Layout **manda** usar
|
||||||
|
`board.appendChild`. La política correcta es condicional, y hay 5 seeds del grupo A3 dedicados a ella.
|
||||||
|
|
||||||
|
**Z-order:** el orden del array `children`. Métodos: `bringToFront()`, `sendToBack()`,
|
||||||
|
`bringForward()`, `sendBackward()`, `setParentIndex(index)` (0-based).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Layouts
|
||||||
|
|
||||||
|
### Flex
|
||||||
|
|
||||||
|
| Miembro | Estado | Detalle |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `board.addFlexLayout()` | ✅ | Devuelve `FlexLayout` |
|
||||||
|
| `board.flex` | ✅ | Guard: `if (board.flex)`. **`null`** si no hay layout |
|
||||||
|
| `dir` | ✅ | `'row'` \| `'row-reverse'` \| `'column'` \| `'column-reverse'` |
|
||||||
|
| `rowGap`, `columnGap` | ✅ | **No existe el shorthand `gap`** |
|
||||||
|
| `alignItems`, `alignContent`, `justifyItems`, `justifyContent` | ✅ | |
|
||||||
|
| `topPadding`/`rightPadding`/`bottomPadding`/`leftPadding`, `verticalPadding`/`horizontalPadding` | ✅ | |
|
||||||
|
| `horizontalSizing` / `verticalSizing` | ⚠️ | `'fill'` \| `'auto'` \| `'fit-content'`. **`'auto'` se lee de vuelta como `'auto'` pero el board NO crece** (penpot#8520). Hay que `board.resize()` a mano |
|
||||||
|
| `wrap` | ✅ | `'wrap'` \| `'nowrap'` |
|
||||||
|
| `flex.appendChild` | ❌ | **Roto.** Usar `board.appendChild` |
|
||||||
|
|
||||||
|
### Grid
|
||||||
|
|
||||||
|
| Miembro | Estado | Detalle |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `board.addGridLayout()` | ✅ | **No `addFlexLayout()`**, como dice erróneamente el overview |
|
||||||
|
| `board.grid` | ✅ | Guard: `if (board.grid)` |
|
||||||
|
| `addRow(type, value?)`, `addColumn(type, value?)` | ✅ | `type`: `'flex'` \| `'fixed'` \| `'percent'` \| `'auto'` |
|
||||||
|
| `addRowAtIndex`, `addColumnAtIndex`, `removeRow`, `removeColumn`, `setRow`, `setColumn` | ✅ | |
|
||||||
|
| `rows`, `columns` | ✅ | `Track[]` = `{type, value}` |
|
||||||
|
| **`grid.appendChild(shape, row, column)`** | ✅ | **Índices 1-based.** `(s, 0, 0)` se clampea y se lee `[1,1]`; `(s, 1, 2)` se lee `[1,2]`. El ejemplo 0-based de la doc de tipos está mal |
|
||||||
|
| `shape.layoutCell` | ✅ | `{row, rowSpan, column, columnSpan, areaName, position}` — para leer de vuelta y reposicionar |
|
||||||
|
|
||||||
|
### `layoutChild` (el hijo dentro de un board con layout)
|
||||||
|
|
||||||
|
| Miembro | Estado | Detalle |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `layoutChild` | ✅ | **Existe solo DESPUÉS de insertar** el shape en un board con layout |
|
||||||
|
| `horizontalSizing` / `verticalSizing` | ✅ | `'fill'` \| `'auto'` \| **`'fix'`** (no `'fixed'`, no `'fit-content'`) |
|
||||||
|
| `alignSelf` | ✅ | |
|
||||||
|
| `absolute` | ✅ | `true` saca al hijo del flujo del layout |
|
||||||
|
| `*Margin`, `maxWidth`/`maxHeight`/`minWidth`/`minHeight` | ✅ | |
|
||||||
|
| **`zIndex`** | ⚠️ | Está en el tipo pero **se ignora en silencio** (se setea 10, se lee 0). Usar el orden de `children` |
|
||||||
|
|
||||||
|
**Invariante R8:** todo efecto de layout se **lee de vuelta** del objeto retornado. El seed se entrena
|
||||||
|
sobre `"titleStretched": true` leído del runtime, no sobre la esperanza de que haya funcionado.
|
||||||
|
|
||||||
|
**Invariante R9:** wrappers abrazan (`flex.horizontalSizing = 'fit-content'`), secciones llenan
|
||||||
|
(`layoutChild.horizontalSizing = 'fill'`). Los dos idioms en el mismo seed para que el contraste sea
|
||||||
|
aprendible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Imágenes
|
||||||
|
|
||||||
|
| Miembro | Estado |
|
||||||
|
| --- | --- |
|
||||||
|
| `await penpot.uploadMediaUrl(name, url)` → `ImageData` | ✅ **Única vía** |
|
||||||
|
| `await penpot.uploadMediaData(name, bytes, mimeType)` | ✅ |
|
||||||
|
| `fills = [{fillOpacity: 1, fillImage: img}]` | ✅ |
|
||||||
|
| `img.keepAspectRatio = true` | ✅ Para fotos |
|
||||||
|
| `penpot.createImage()` | ❌ No existe |
|
||||||
|
| `penpotUtils.importImage()` | ❌ `penpotUtils.importImage is not a function` |
|
||||||
|
| herramienta MCP `import_image` | ❌ No existe en este deployment |
|
||||||
|
|
||||||
|
`uploadMediaUrl` devuelve una `Promise` y puede rechazar (`Error uploading media`): `await` dentro de
|
||||||
|
un `try/catch`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Biblioteca local (tokens de diseño)
|
||||||
|
|
||||||
|
| Miembro | Estado | Detalle |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `penpot.library.local` | ✅ | |
|
||||||
|
| `library.local.createColor()` | ✅ | Luego `c.name = 'Brand/Primary'; c.color = '#D62828';` |
|
||||||
|
| `libraryColor.asFill()` | ✅ | Devuelve un `Fill` con `fillColorRefFile`/`fillColorRefId` |
|
||||||
|
| `libraryColor.asStroke()` | ✅ | |
|
||||||
|
| `library.local.createTypography()` | ✅ | |
|
||||||
|
| `library.local.createComponent(shapes)` | ✅ | |
|
||||||
|
| `component.instance()` / `mainInstance()` | ✅ | |
|
||||||
|
| `penpot.library.connected`, `availableLibraries()`, `connectLibrary(id)` | ✅ | |
|
||||||
|
| **`typography.setFont(font, variant?)`** | ❌ | Está en el tipo, **no existe en el runtime**: `t.setFont is not a function`. Setear `fontFamily`/`fontSize`/`fontWeight`/`lineHeight` una por una |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Verificación programática (invariante R12)
|
||||||
|
|
||||||
|
| Miembro | Estado | Uso |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `penpot.generateStyle(shapes, {type:'css', includeChildren:true})` | ✅ | Devuelve CSS real. **La opción es `includeChildren`; `withChildren` (que usa el overview) se ignora** |
|
||||||
|
| `penpot.generateMarkup(shapes, {type:'html'})` | ✅ | Devuelve HTML real |
|
||||||
|
| `penpotUtils.analyzeDescendants(root, evaluator, maxDepth?)` | ✅ | Auditoría; el evaluador puede devolver funciones correctoras |
|
||||||
|
| `penpotUtils.isContainedIn(shape, container)` | ✅ | Violaciones de contención |
|
||||||
|
| `penpotUtils.shapeStructure(shape, maxDepth)` | ✅ | Overview del árbol. **Su clave `layout` es del output, no del shape** |
|
||||||
|
| herramienta `export_shape` | ✅ | Inspección visual |
|
||||||
|
|
||||||
|
**R12: nunca terminar sin mirar y medir.** Todo seed compositivo cierra con `export_shape` + una
|
||||||
|
auditoría programática, y el mensaje final **cita los números** de esa auditoría.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Contramedida al system prompt del servidor
|
||||||
|
|
||||||
|
El servidor MCP inyecta verbatim (ver `penpot_system_prompt.md`):
|
||||||
|
|
||||||
|
> NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own
|
||||||
|
> colours and stick to non-creative defaults such as white/black if you are lacking information).
|
||||||
|
|
||||||
|
**Ninguno de los 41 seeds viejos tiene mensaje `system`**, así que el modelo nunca se entrenó en
|
||||||
|
presencia de esta instrucción. En producción la aplica al revés y produce el gris.
|
||||||
|
|
||||||
|
Obligatorio en los seeds nuevos:
|
||||||
|
|
||||||
|
1. El bloque `instructions` **verbatim** como mensaje `system` en **~30 %** de los seeds (el resto sin
|
||||||
|
él, para robustez en las dos condiciones).
|
||||||
|
2. En **todo** seed creativo, `reasoning_content` que desambigüe explícitamente. La regla condiciona
|
||||||
|
sobre *"transferring styles from a Penpot design to code"*; crear un diseño nuevo es el caso
|
||||||
|
contrario: no hay diseño previo del que transferir, así que no hay "missing values" sobre los que
|
||||||
|
asumir. Elegir una paleta deliberada **es** la tarea.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Los doce invariantes anti-caja-gris
|
||||||
|
|
||||||
|
Cada uno presente **en los datos** y verificado por `scripts/07_lint_penpot_code.py`, no solo enunciado
|
||||||
|
en prosa.
|
||||||
|
|
||||||
|
| # | Invariante |
|
||||||
|
| --- | --- |
|
||||||
|
| R1 | Todo shape creado recibe `fills` explícito **antes** de insertarse. Los wrappers puros de layout ponen `fills = []` — transparente **por intención**, nunca "sin tocar" |
|
||||||
|
| R2 | **Cero grises de placeholder.** Todo hex debe pasar `saturación > 10` **o** `max < 100` **o** `max > 220`. El gris solo aparece en los tool results del grupo B8, y solo como *input* |
|
||||||
|
| R3 | Toda paleta es un set cerrado y nombrado de ≤ 8 roles, declarado al inicio del payload y referenciado por rol |
|
||||||
|
| R4 | Todo texto recibe `characters` + tamaño + fill + `growType` |
|
||||||
|
| R5 | Tamaño, peso e interlineado son **strings** entre comillas |
|
||||||
|
| R6 | Tras `resize()` sobre un texto, restaurar `growType` |
|
||||||
|
| R7 | Una card es fondo + radio + elevación |
|
||||||
|
| R8 | Todo hijo de layout declara su sizing, y el efecto se **lee de vuelta** del objeto retornado |
|
||||||
|
| R9 | Wrappers abrazan (`fit-content`), secciones llenan (`fill`) — los dos idioms en el mismo seed |
|
||||||
|
| R10 | Escala tipográfica, nunca tamaños ad-hoc: ≥ 3 tamaños distintos por pantalla compuesta |
|
||||||
|
| R11 | Espaciado en escala de 8 px |
|
||||||
|
| R12 | Nunca terminar sin mirar y medir: `export_shape` → auditoría → resumen que **cita los números** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Resumen de patrones prohibidos (lo que el lint busca)
|
||||||
|
|
||||||
|
| Patrón | Por qué |
|
||||||
|
| --- | --- |
|
||||||
|
| `findShapeById(` con coma en los argumentos | Aridad 1; la forma de 2 devuelve `null` en silencio |
|
||||||
|
| `.layout` en una línea sin `shapeStructure` | La propiedad no existe |
|
||||||
|
| `.flex.appendChild(` | Roto |
|
||||||
|
| `appendChild(` sobre un receptor sin evidencia de flex | Solo es correcto en boards con flex |
|
||||||
|
| `fontSize`/`fontWeight`/`lineHeight`/`letterSpacing` `= <número>` | Son strings |
|
||||||
|
| `textAlign =` | Lanza y mata el `execute_code` |
|
||||||
|
| `importImage`, `import_image`, `createImage(`, `filePath` | No existen en este deployment |
|
||||||
|
| Asignación a `width`/`height`/`parentX`/`parentY`/`bounds` | Read-only |
|
||||||
|
| `gap =` | No hay shorthand; son `rowGap`/`columnGap` |
|
||||||
|
| `shadows = [{... color: {fillColor` | `Shadow.color` es un `Color`, no un `Fill` |
|
||||||
|
| Hex gris de placeholder fuera del grupo B8 | Invariante R2 |
|
||||||
|
| `console.log` de algo que también se retorna | Prohibición explícita del servidor |
|
||||||
|
| `setFont(` | No existe en el runtime |
|
||||||
|
| `createText()` / `createText('')` | Devuelven `null` |
|
||||||
|
| `high_level_overview` en más de 2 seeds | Su descripción prohíbe llamarlo dos veces |
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
|||||||
|
# Penpot MCP — strings de error reales
|
||||||
|
|
||||||
|
Mensajes de error **literales** que devuelve el servidor MCP de Penpot cuando un `execute_code` falla.
|
||||||
|
|
||||||
|
**Por qué existe este archivo.** El grupo D de `data/raw/seeds/penpot.jsonl` (6 seeds de auto-corrección)
|
||||||
|
entrena al modelo a diagnosticar y recuperarse de errores. Si el mensaje de error de un seed es
|
||||||
|
inventado, el modelo aprende a reaccionar a un string que nunca va a ver. Este archivo es la
|
||||||
|
**allow-list de mensajes de error**: `scripts/07_lint_penpot_code.py` verifica que todo `content` de un
|
||||||
|
mensaje `tool` que representa un error sea un string presente acá, verbatim.
|
||||||
|
|
||||||
|
**Procedencia.** Los mensajes de las secciones 1-4 fueron capturados en vivo por la sesión madre el
|
||||||
|
2026-07-30, contra el archivo real del usuario, ejecutando el código de la columna "Disparador" vía
|
||||||
|
`mcp__penpot__execute_code`. Están registrados en `PLAN.md` § "La API verificada" y en la página de
|
||||||
|
Docmost de la Fase 6 § "Diagnóstico". Los de la sección 5 son mensajes estándar del runtime de
|
||||||
|
JavaScript (V8/SpiderMonkey), deterministas dada la expresión que los produce.
|
||||||
|
|
||||||
|
**Nota operativa (2026-07-30):** al intentar re-capturar estos strings durante el paso 6.1.3, el
|
||||||
|
servidor MCP devolvió `No Penpot plugin instances are currently connected. Please ensure the plugin is
|
||||||
|
running and connected.` — el plugin del usuario no está conectado. Los mensajes de abajo se conservan
|
||||||
|
tal como los verificó la sesión madre; **la re-verificación en vivo queda pendiente** y se hace junto
|
||||||
|
con el baseline de la puerta 5 (paso 6.3.17), que también requiere el plugin conectado. Ningún seed
|
||||||
|
puede usar un string de error ausente de este archivo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Propiedad inexistente sobre un objeto no extensible
|
||||||
|
|
||||||
|
El objeto `Text` (y en general los shapes) **no es extensible**: asignar una propiedad que no está en
|
||||||
|
la interfaz lanza, y la excepción **aborta todo el `execute_code`**, no solo esa línea.
|
||||||
|
|
||||||
|
| Disparador | Mensaje verbatim |
|
||||||
|
| --- | --- |
|
||||||
|
| `text.textAlign = 'center'` | `Cannot add property textAlign, object is not extensible` |
|
||||||
|
|
||||||
|
Forma del mensaje, generalizable: `Cannot add property <nombre>, object is not extensible`.
|
||||||
|
|
||||||
|
La propiedad correcta para alineación horizontal de texto es **`align`**
|
||||||
|
(`'left' \| 'center' \| 'right' \| 'justify'`). Ver `penpot_api_docs.md` Captura 4.
|
||||||
|
|
||||||
|
**Este es el error más caro del conjunto**, porque el modelo suele escribir `textAlign` por analogía con
|
||||||
|
CSS, en medio de un payload de 80 líneas que construye una landing entera: la excepción tira abajo la
|
||||||
|
composición completa, no solo el título.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Función inexistente en el runtime
|
||||||
|
|
||||||
|
| Disparador | Mensaje verbatim |
|
||||||
|
| --- | --- |
|
||||||
|
| `penpotUtils.importImage(url)` | `penpotUtils.importImage is not a function` |
|
||||||
|
| `typography.setFont(font)` sobre un `LibraryTypography` | `t.setFont is not a function` |
|
||||||
|
|
||||||
|
Dos casos distintos, y la diferencia importa pedagógicamente:
|
||||||
|
|
||||||
|
- `penpotUtils.importImage` **nunca existió**: no está en la Captura 1 de `penpot_api_docs.md`, ni
|
||||||
|
`import_image` está entre las herramientas MCP de este deployment (ver `data/schemas/penpot.json`).
|
||||||
|
El overview la menciona en prosa (`Use the export_shape and import_image tools`), lo cual es un
|
||||||
|
**anzuelo real**: la lista de herramientas que recibe el modelo es autoritativa, la prosa del
|
||||||
|
overview no. La ruta correcta es `await penpot.uploadMediaUrl(name, url)`.
|
||||||
|
- `typography.setFont` **sí está en el tipo** (`penpot_api_docs.md` Captura 26) pero **no existe en el
|
||||||
|
runtime de esta versión**. Es el caso canónico de "la documentación no es el runtime": hay que setear
|
||||||
|
las propiedades de tipografía una por una (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`).
|
||||||
|
Notar que el mensaje dice `t.setFont`, con el nombre de la variable local — no `typography.setFont`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Uso de un retorno `null` sin chequear
|
||||||
|
|
||||||
|
La familia de errores más frecuente y la que produce la falla reportada por el usuario. Todas las
|
||||||
|
llamadas de creación y de búsqueda pueden devolver `null`; usar el resultado sin chequearlo produce un
|
||||||
|
`TypeError` en la **línea siguiente**, lo que hace que el stack apunte al síntoma y no a la causa.
|
||||||
|
|
||||||
|
| Disparador | Mensaje verbatim |
|
||||||
|
| --- | --- |
|
||||||
|
| `const t = penpot.createText(); t.characters = 'Hola'` | `Cannot set properties of null (setting 'characters')` |
|
||||||
|
| `const s = penpotUtils.findShapeById(page, id); s.x = 10` | `Cannot read properties of null (reading 'x')` |
|
||||||
|
| `const s = penpotUtils.findShapeById(idInexistente); return s.fills` | `Cannot read properties of null (reading 'fills')` |
|
||||||
|
| `const b = penpot.createBoolean('union', []); b.name = 'x'` | `Cannot set properties of null (setting 'name')` |
|
||||||
|
|
||||||
|
Forma del mensaje, generalizable:
|
||||||
|
`Cannot read properties of null (reading '<prop>')` y
|
||||||
|
`Cannot set properties of null (setting '<prop>')`.
|
||||||
|
|
||||||
|
**Las tres fuentes de `null` que los seeds deben enseñar a chequear:**
|
||||||
|
|
||||||
|
1. `penpot.createText()` **sin argumento** o con `''` → `null`. Con un string no vacío → `Text`.
|
||||||
|
El ejemplo de la documentación oficial (`penpot_api_docs.md` Captura 6) usa la forma sin argumento.
|
||||||
|
2. `penpotUtils.findShapeById(page, id)` — **la forma de dos argumentos no lanza**: devuelve `null` en
|
||||||
|
silencio, porque `findShapeById` tiene aridad 1 y el segundo argumento se descarta mientras el
|
||||||
|
primero (un `Page`, no un `string`) no matchea ningún id. La forma correcta es
|
||||||
|
`penpotUtils.findShapeById(id)`.
|
||||||
|
3. `penpot.createBoolean(type, shapes)` y `penpot.createShapeFromSvg(svg)` declaran `null | T` en su
|
||||||
|
firma (Capturas 10 y 11).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Fallos de red y de subida de medios
|
||||||
|
|
||||||
|
| Disparador | Mensaje verbatim |
|
||||||
|
| --- | --- |
|
||||||
|
| `await penpot.uploadMediaUrl('foto', 'https://dominio-inexistente.invalid/a.jpg')` | `Error uploading media` |
|
||||||
|
|
||||||
|
`uploadMediaUrl` devuelve una `Promise`; el fallo llega como rechazo, así que hay que envolverlo en
|
||||||
|
`try/catch` **con `await` adentro del `try`** (un `.catch()` colgado de la promesa sin `await` deja el
|
||||||
|
resto del payload corriendo con `undefined`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Errores estándar del runtime de JavaScript
|
||||||
|
|
||||||
|
Deterministas dada la expresión, no específicos de Penpot. Se listan porque aparecen en los seeds del
|
||||||
|
grupo D y el lint los tiene que aceptar.
|
||||||
|
|
||||||
|
| Disparador | Mensaje verbatim |
|
||||||
|
| --- | --- |
|
||||||
|
| `undefined.foo` | `Cannot read properties of undefined (reading 'foo')` |
|
||||||
|
| `shapes.map(...)` donde `shapes` es `null` | `Cannot read properties of null (reading 'map')` |
|
||||||
|
| `JSON.parse('{')` | `Unexpected end of JSON input` |
|
||||||
|
| `board.appendChild()` sin argumento | `Cannot read properties of undefined (reading 'id')` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Errores a nivel de servidor MCP (no de `execute_code`)
|
||||||
|
|
||||||
|
No son excepciones de JavaScript: los devuelve el servidor MCP antes de llegar al plugin. **No pueden
|
||||||
|
aparecer como resultado de un `execute_code` en un seed**, porque no dejan al modelo en una situación
|
||||||
|
de "diagnosticá tu código" — significan que no hay con qué hablar.
|
||||||
|
|
||||||
|
| Situación | Mensaje verbatim |
|
||||||
|
| --- | --- |
|
||||||
|
| El plugin de Penpot no está abierto / no está conectado al servidor MCP | `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.` |
|
||||||
|
|
||||||
|
Capturado en vivo el 2026-07-30 durante el paso 6.1.3 de esta fase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Fallos SILENCIOSOS — no producen ningún error
|
||||||
|
|
||||||
|
**La categoría más peligrosa**, y la razón por la que el invariante R8 (`leer de vuelta el efecto`)
|
||||||
|
existe. Estas operaciones **no lanzan**, **no devuelven `null`**, y dejan el diseño mal. Un modelo que
|
||||||
|
solo maneja excepciones no las detecta nunca.
|
||||||
|
|
||||||
|
| Operación | Comportamiento real | Cómo detectarlo |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `flex.horizontalSizing = 'auto'` | Se lee de vuelta como `'auto'` pero **el board no crece** (200×200 sigue 200×200 tras insertar un hijo de 300 de ancho). Bug conocido penpot#8520 | Leer `board.width` tras insertar; hacer `board.resize()` a mano |
|
||||||
|
| `layoutChild.zIndex = 10` | Se lee de vuelta como `0`. **Se ignora** | Leer `layoutChild.zIndex`; usar el orden del array `children` para z-order |
|
||||||
|
| `text.letterSpacing = '-0.02'` | Se lee de vuelta como `"0"`. Los valores negativos en unidades tipo `em` **se descartan** | Leer `text.letterSpacing`; usar unidades tipo px (`'-1'`) |
|
||||||
|
| `penpot.createRectangle()` sin asignar `fills` | Queda con el fill por defecto **`#B1B2B5`** — el gris exacto del problema reportado | Invariante R1: `fills` explícito en todo shape creado |
|
||||||
|
| `penpotUtils.findShapeById(page, id)` | Devuelve `null`, **sin lanzar** | Chequear el retorno antes de usarlo |
|
||||||
|
| `board.layout` | `undefined`; `'layout' in shape === false`. La propiedad **no existe** | Usar `board.flex` / `board.grid`, que devuelven `null` cuando no hay layout |
|
||||||
|
| `generateStyle(shapes, {withChildren: true})` | La opción real es `includeChildren`; `withChildren` **se ignora** y el CSS sale sin los hijos | Ver `penpot_api_docs.md` Captura 8 |
|
||||||
|
| `grid.appendChild(shape, 0, 0)` | El `0` **se clampea a 1**: se lee de vuelta `[1,1]`. Los índices son 1-based | Leer `shape.layoutCell.row` / `.column` |
|
||||||
|
| `text.resize(w, h)` | Setea `growType` a `'fixed'` en silencio; el texto desborda su caja | Invariante R6: restaurar `growType` tras `resize()` |
|
||||||
|
| Leer `text.width` justo después de setear `characters` | El auto-sizing **no es inmediato**: se lee `1` | Dormir ~120 ms antes de leer el bounding box |
|
||||||
@@ -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".
|
||||||
Reference in New Issue
Block a user