# 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.flex.appendChild(shape)` (preferido, orden visual) o `board.appendChild(shape)` (orden invertido, ver abajo) | | 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 | **ACTUALIZACIÓN (2026-08-04, hallazgo 54 de la Fase 6, ronda correctiva 6.4.25c): `board.flex.appendChild(shape)` NO está roto — verificado en vivo, repetidas veces, contra Penpot 2.16.2.** Es una función real, de aridad 1, distinta de `board.appendChild` (no es un alias: `board.flex.appendChild !== board.appendChild`, `board.flex !== board`), que no lanza ninguna excepción y agrega el hijo correctamente, con `layoutChild` funcionando bien después. La afirmación anterior de que estaba roto **nunca tuvo un error real capturado** en `penpot_errors.md` (a diferencia de todos los demás comportamientos rotos documentados en este archivo, que sí lo tienen). Su origen real: el `high_level_overview()` del servidor MCP (capturado verbatim en `penpot_api_docs.md`, líneas 131-135) le dice al modelo, en mayúsculas y marcado `CRITICAL`, que **`board.flex.appendChild` está BROKEN y que NUNCA lo use** — instrucción que la sesión original de diagnóstico tomó como cierta sin verificarla empíricamente con un intento real de romperlo. Esa misma sección de la doc mezcla además un bug real y distinto (el ejemplo de **grid** con 3 argumentos aparece escrito como `board.flex.appendChild(shape, row, column)` en vez de `board.grid.appendChild(...)`, ver `penpot_api_docs.md` línea 143), lo cual reforzó la confusión. **La documentación servida en vivo por el MCP de Penpot está, en este punto, objetivamente equivocada** (verificado empíricamente, repetidas veces, el 2026-08-04 contra Penpot 2.16.2) — o el comportamiento se arregló en Penpot después de escribirse esa documentación, sin actualizarla. Cualquier seed nuevo que muestre al modelo leyendo `high_level_overview` debería, idealmente, incluir el razonamiento de que esa afirmación puntual sobre `flex.appendChild` no es confiable y de que hay que verificar con el runtime en vez de creerle a esa línea específica — el mismo patrón de "la lista de tools es autoritativa, la prosa no" ya usado en el grupo C para `import_image`. **Orden invertido, verificado — y por qué `board.flex.appendChild` es preferible.** En `dir: 'column'` (y `'row'`), apendeando PRIMERO y luego SEGUNDO con `board.appendChild`, el array `children` queda `[SEGUNDO, PRIMERO, ...]`: `board.appendChild` inserta **al frente**, así que hay que llamarlo en orden inverso al visual para compensar. `board.flex.appendChild`, en cambio, preserva el **orden visual normal** de las llamadas (PRIMERO, SEGUNDO, ...) — no necesita el truco de inversión. Para boards con flex, **usar `board.flex.appendChild(shape)` en orden visual directo es la forma más simple y menos propensa a error.** `board.appendChild` sigue siendo válido (y necesario para reparentar hacia boards sin layout, o cuando el código ya maneja el truco de inversión de forma consistente), pero ya no es la única opción recomendada. **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 — esos seeds siguen siendo correctos sobre el comportamiento de `board.appendChild` (el orden invertido es real), pero ya no reflejan que `board.flex.appendChild` sea una alternativa rota. **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` | ✅ | **Funciona** (verificado 2026-08-04, hallazgo 54). Aridad 1, preserva orden visual — preferible a `board.appendChild` para insertar en boards con flex | ### 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 | | `appendChild(` sobre un receptor sin evidencia de flex | Solo es correcto en boards con flex (`board.flex.appendChild` o `board.appendChild`) | | `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 |