issue-210: Implement the penpot_add_svg tool with svg-raw fallback and conversion reporting #243
No reviewers
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills!243
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/issue-210/implement-the-penpot-add-svg-tool-with-svg-raw-fal"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
210: Implement the penpot_add_svg tool with svg-raw fallback and conversion reporting
Closes #210 — 210.
Generated in 40m 43s.
Review findings
I'll start by exploring the repository structure and the relevant files.Now let me implement the fixes. Starting with the critical circle geometry in
src/svg.ts:Now the tool file. First the header, schema, conversion error mapping and circle transform:Now the result text notes and the input/details wiring:Now let me add the tests. First the assertions in existing tests:Now the documentation surfaces. Root README:Now the penpot extension README row (correcting thesvg-rawclaim) and CHANGELOG:Now the skill workflow doc (Medium finding):All fixes are in place and the suite is green apart from a pre-existing unrelated failure. Here is the annotated findings document.Review Findings — 210
Summary
Fixed every finding in a single pass. Critical: the
<circle>path now stages a real box —CircleGeometrygainedx/y/width/height(built by a new sharedcircleGeometryFrom, used by bothapplyMatrixToCircleandtransformCircle) andbuildShapeForDescriptorspreads the geometry's ownselrect/points, so an SVG with a<circle>can no longer produce an invalidupdate-filepayload; the vacuous test now asserts concrete numbers (x === 8,width === 8, the full selrect/points). High: thesvg-rawclaim is corrected everywhere it was model-facing (tool header, tool description, an explicitformatAddSvgTextnote, the extension README, the CHANGELOG and the skill), taking the finding's sanctioned documentation option rather than adding a transit writer; tests were added forhidden/opacitystaging and for both linked-library read-failure paths (continue on a failed libraryget-file, abort on a failedget-file-libraries). Medium: root README tool list and repo layout updated,SKILL.mdcompose/lossy sections updated, the element-budgetSvgErrornow maps to its ownpenpot_add_svgmessage (no dead-endmaxNodesadvice), and the SVGfont-familyis no longer copied over the defaultfontId/fontVariantId— the dropped families are reported in the text and indetails.droppedFontFamilies. Low:scaleusesexclusiveMinimum: 0,describeCountsno longer double-countssvg-rawas converted, and a zero-shape import states that nothing was staged. Gates:npm test→ 820 tests, 818 pass / 1 fail / 1 skipped; the single failure (scripts/local-install.test.mjs:1860, the mongodbpi.extensionsmanifest check) is pre-existing and unrelated (nopackage.jsonchange here). No build or lint script exists; a stricttscpass overextensions/penpot/src/**was compared against a baselineHEADtree and introduces no new type errors (it removes the 12 circle-geometry read errors). Nothing remains unresolved.Critical
extensions/penpot/src/tools/svg.ts:626— a converted<circle>is staged with nox/y/width/heightand a NaNselrect/points.buildShapeForDescriptorreadsgeometry.x/.y/.width/.height, but a circle descriptor's geometry is aCircleGeometry(src/svg.ts:141, produced byapplyMatrixToCircleatsrc/svg.ts:892andtransformCircleattools/svg.ts:489) which only carriescx/cy/rx/ry/selrect/points, andbaseShapere-derivesselrect/pointsfrom the (undefined) box, discarding the circle's correctselrect. Verified against a stubbed transport:<svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/></svg>stages{"type":"circle","name":"svg dot","selrect":{"x2":null,"y2":null},"points":[{},…]}(JSON.stringifydrops theundefinedkeys and turnsNaNintonull). Penpot requires:x/:y/:width/:height(common/src/app/common/types/shape.cljc:178schema:shape-geom-attrs) and a completegrc/rectselrect, so any SVG containing a<circle>makes the wholeupdate-filebatch fail atpenpot_commit("missing required key: x"), while the tool reports success anddetails.changeCountcounts it as a converted shape. The test meant to catch this (tools/svg.test.ts:640) comparesselrect.xwithobj.x, which are bothundefinedfor a circle, so it passes vacuously. Suggested fix: give circles a real box beforebaseShape— addx: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2toCircleGeometry(set intransformCircleandapplyMatrixToCircle, or normalise it in the tool) and spread the geometry's ownselrect/pointsrather than lettingbaseShapere-derive them; then assert concrete numbers for the circle (e.g.x === 8,width === 8for that fixture) so an undefined geometry can never pass again.High
extensions/penpot/src/tools/svg.ts:22— the header claims "unsupported artwork stays visible and editable-as-an-object", but asvg-rawcreated over this extension's JSON transport renders nothing in Penpot 2.17: the front-end gates the render on(contains? csvg/svg-tags tag)withsvg-tagsa set of keywords (common/src/app/common/svg.cljc:38,frontend/src/app/main/ui/shapes/svg_raw.cljsvalid-tag?/(= tag :svg)/(? svg-tag? valid-tag?), with no:elsebranch), and the backend JSON reader keywordises only keys (backend/src/app/http/middleware.clj:58-60,common/src/app/common/json.cljc:78-95), whileschema:svg-raw-attrsis an empty map socontentis never coerced.findings.md:1605-1627records this, but every model-facing surface still implies the fallback survives visually (the tool description at:825ff, the README row, the CHANGELOG "so no node is dropped silently") and the result text (:733-740) lists the skipped nodes without saying they will not draw. An agent importing a gradient/use/defsicon therefore commits an invisible import and reports success. Suggested fix: either send theupdate-filebody asapplication/transit+jsonwhen the batch containssvg-rawcontent (the middleware already accepts transit —middleware.clj:53), or, if a transit writer is out of scope for this issue, correct the claim here and add an explicit line toformatAddSvgTextsuch as "N node(s) are stored as svg-raw objects; Penpot 2.17 does not render them (its hiccup tag cannot be sent as a keyword over JSON) — treat them as lost artwork and re-work the source or export a raster instead".extensions/penpot/src/tools/svg.ts:636—hidden/opacityare mapped into the staged payload but nothing tests it: a<rect display="none">must stagehidden: trueand an<g opacity="0.5">must stageopacity: 0.5(verified by probe), yetsvg.test.tshas no occurrence of either field, so the documented "a hidden layer does not reappear as visible artwork" guarantee (README/CHANGELOG) can regress silently. Suggested fix: add a test importingPRIMITIVES_SVGplus adisplay:nonenode and anopacity="0.5"group and assertobj.hidden === true/obj.opacity === 0.5on the stagedadd-objpayloads.extensions/penpot/src/tools/svg.ts:383— the documented resilience path "a linked library that cannot be read is recorded and the import continues" has no test: the!result.okbranch and itslibraryReadFailuresreporting (plus the hardfailToolon aget-file-librariesfailure at:369) are only reachable through a stub that 500s a libraryget-file. Suggested fix: add a test whoseget-file-librarieslists a library id, whoseget-filefor that id returns 500, and assert the import still stages every shape, thatdetails.colors.libraryReadFailuresnames the library, and that the message reaches the result text (a second test can assert theget-file-librariesfailure aborts with the server error and stages nothing).Medium
README.md:39— the root README enumerates every registered penpot tool (penpot_add_image,penpot_instance_component, …) and was updated by the previous tool issues, but this change updates onlyextensions/penpot/README.md;penpot_add_svgis missing here and in the repository-layout line atREADME.md:145. Suggested fix: addpenpot_add_svg(imports an SVG as native shapes with ansvg-rawfallback and conversion/colour-matching report) to the tool enumeration atREADME.md:39and to theextensions/penpot/line atREADME.md:145.skills/penpot/SKILL.md:69— the skill's "Compose the screen" list (the place an agent following the workflow looks) listspenpot_add_frame/add_rect/add_text/add_image/instance_componentbut notpenpot_add_svg, even though the skill's own description advertises composing "from a prompt, an SVG or a screenshot" and plan milestone M8 requires the three creation paths to be documented. Suggested fix: add a bullet forpenpot_add_svg(path or inline markup, native subset +svg-rawfallback,parent_iddefaults to the root frame,link_colorsmatches library swatches) and a line in the "Lossy areas" section that unsupported nodes are stored assvg-rawand do not render in 2.17.extensions/penpot/src/tools/svg.ts:323— the converter's element-budget overflow is re-labelled with the wrong cause and an unactionable remedy: a large-but-valid SVG throws "the SVG is not a valid SVG document: SVG exceeds the 10000-element limit; raise maxNodes to convert it" (src/svg.ts:1691), butpenpot_add_svgexposes nomaxNodesparameter, so the caller has no way to follow the advice and is told a valid document is invalid. Suggested fix: map the budgetSvgErrorto its own message inconvertMarkup(e.g. "the SVG has more than 10 000 elements, which penpot_add_svg cannot import — simplify the source or split it"), and either raise/bound the limit explicitly or expose it as a tool parameter.extensions/penpot/src/tools/svg.ts:664— an SVGfont-familyis copied verbatim into the span whilefontId/fontVariantIdkeep the defaults (sourcesanspro/regular), producing a mismatched font triple that no other tool emits (penpot_add_texthas nofont_familyargument, andpenpot_create_typographyresolves the family against the instance's variants and records a substitution). Penpot loads text faces by(juxt :font-id :font-variant-id)(frontend/src/app/main/ui/shapes/text.cljs:17-35), so a family the instance does not have renders with a face that does not match the family shown for the layer, silently and with no substitution reported. Suggested fix: resolve the family through the same font path aspenpot_create_typography(recording the substitution indetails/the result text, likepenpot_add_text's substitution note) or leave the span on the documented default triple and report the dropped family as a note.Low
extensions/penpot/src/tools/svg.ts:142—scaledeclaresminimum: 0while:825requires> 0, soscale: 0passes schema validation and is then rejected by a hand-rolled error, while a negative value is rejected by typebox with a different message. Suggested fix: useType.Number({ exclusiveMinimum: 0, ... })and drop the manual check (or keep both with identical wording).extensions/penpot/src/tools/svg.ts:728—describeCounts(report.counts)includes thesvg-rawbucket, so a fallback-only import reads "Converted: 0 (svg-raw 2)" immediately above "Fallback (svg-raw): 2" (verified), double-reporting the fallbacks as converted. Suggested fix: filtersvg-rawout ofdescribeCounts(or rename the parenthetical to "shapes by type" and keep the counts disjoint from theConverted:number).extensions/penpot/src/tools/svg.ts:725— a valid source that yields no shapes (<svg viewBox="0 0 24 24"/>,<svg/>) returns a plain success naming "Staged 0 shapes" withchangeCount: 0and no hint that nothing was imported, which is easy for a model to miss. Suggested fix: wheninput.status.changeCount === 0, append a line such as "The SVG contained no importable artwork — nothing was staged (an empty or defs-only document is not an error)".