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.
This commit is contained in:
@@ -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