issue-208: Validate component instancing against the live instance #241
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!241
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/issue-208/validate-component-instancing-against-the-live-ins"
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?
208: Validate component instancing against the live instance
Closes #208 — 208.
Generated in 18m 08s.
Review findings
I'll start by exploring the repository state and the relevant files.Applied the feasible findings, re-ran the gates (stub harness 19/19,
node --test688 tests with only the pre-existing unrelatedscripts/local-install.test.mjs:1875failure), and verified the new harness checks are non-vacuous by temporarily reverting the strip.Review Findings — 208
Summary
The diff is a validation change for
penpot_instance_component: it stripstouched/variantId/variantNamefrom each clone inbuildInstance(matching Penpot 2.17.2'smake-component-instance), adds a unit regression test, adds harness check 8 plus a stub libraryButton, and documents the pass infindings.md/README.md/CHANGELOG.md. Gates: build — none configured (no roottsconfig/build step; TS is type-stripped bynode --testand the pi loader); lint/typecheck — none configured (no eslint/prettier/biome/tsc in the repo); test —npm testruns the stub harness (check 8 passes 16/16) plusnode --test: 682 pass, 1 fail, and the failure is pre-existing and unrelated (scripts/local-install.test.mjs:1875assertsextensions/mongodb/index.tsis inpackage.json'spi.extensions; it is absent and neither file is touched by this diff — confirmed againstgit show bbd1ae3:package.json). The penpot suites alone are 556 pass / 0 fail. The substantive gap is that the issue's own deliverable — validation against the live instance — did not happen; the static comparison it did perform is sound but missed at least one further deviation from Penpot's clone (swap-keep attrs), which I reproduced locally.Remediation: fixed all of High, Medium and Low.
buildInstancenow removes the root's swap-keep attributes (restoring only anautosizing when the root drives a layout), setsignoreTouched: trueon every change, and stripsmainInstancefrom every clone; harness check 8 asserts the three stripped instance-state fields and compares asset refs against the main shape, and the stub'sButtonroot now carriestouched/variantId/variantNameso the strip is exercised end to end (19/19, and reverting the deletes makes those checks fail); thefindings.md"never accepted" claim is qualified and thePENPOT_SKILL_DESIGN.md/PENPOT_SKILL_IMPLEMENTATION_PLAN.mdrecord is reconciled. The parent-inside-a-copy part of theignoreTouchedfinding is explicitly documented (it needs the target file's shape tree, which the pure function does not receive) rather than half-implemented. The Critical finding remains: this environment has noPENPOT_URL/PENPOT_TOKENand no browser, so the live validation cannot be performed here.Critical
get-filere-read and no UI observation was performed, socomponentId/componentFile/componentRoot/shapeRef, main-component propagation, override survival and unlink behaviour remain unverified against the real server. Every client-visible claim in the same document is the version from my earlier investigation and I have not independently re-checked it."). Suggested fix: runPENPOT_URL=… PENPOT_TOKEN=… PENPOT_SCRATCH_FILE_ID=… PENPOT_LIBRARY_FILE_ID=… node extensions/penpot/scripts/live-write-check.mjs(check 8) from an environment that has the token — the 2026-09-14 pass recorded indocs/PENPOT_SKILL_DESIGN.md:329anddocs/PENPOT_SKILL_IMPLEMENTATION_PLAN.md:9shows the live instance and a token have been available outside the implement environment — then execute and record the printed UI checklist infindings.md; if that is genuinely impossible, mark the issue blocked/validated-by-static-analysis-only rather than closing it as validated.High
layoutItemMargin,layoutItemMarginType,layoutItemHSizing,layoutItemVSizing,layoutItemMaxH/MinH/MaxW/MinW,layoutItemAbsolute,layoutItemZIndex,layoutItemAlignSelf,interactions), which Penpot's own instantiate path deliberately removes from the component root before cloning (remove-swap-keep-attrs,common/src/app/common/types/container.cljc:287-298, applied at:321). Reproduced withbuildInstanceon a main root carryinglayoutItemAbsolute: true+interactions: both survive into the instance root. Consequence: an instance dropped into an auto-layout frame inherits the library's absolute positioning/margins/z-index instead of behaving as a fresh child, andinteractionscopied into the consumer file reference shape ids that belong to the library file. Suggested fix: in theisRootbranch delete the twelve swap-keep keys fromcloned, then re-setlayoutItemHSizing/layoutItemVSizingto"auto"only when the library root has a layout (any-layout?) and that sizing was"auto"— mirroringremove-swap-keep-attrs— and add ainstance.test.tscase asserting the root clone drops the non-auto ones while a child'slayoutItem*is preserved.Medium
add-objchanges carry noignoreTouched, while Penpot's own instantiate adds every instance shape with{:ignore-touched true}(common/src/app/common/logic/libraries.cljc:287,314; schemachanges.cljc:199; the flag is consumed byctst/add-shape→shape_tree.cljc:42to keep:remote-syncedon a parent that is itself a component copy). Relatedly,generate-instantiate-componentdissocs:component-rootfrom the instance root when the target parent is inside a component copy (libraries.cljc:293-295), whichbuildInstancenever does. Consequence: instantiating into a frame that is part of a component copy marks that copy as locally modified (and produces a nestedcomponent-rootPenpot would not), so the copy stops syncing. Suggested fix: addignoreTouched: trueto each returned change inbuildInstance, and either handle or explicitly document the parent-inside-a-copy case (dropcomponentRootwhen the resolvedparent_id's ancestors include ashape-ref).docs/PENPOT_SKILL_DESIGN.md:329("Component instancing | Created a component from a frame, then built a working instance (componentId+componentRooton the root,shapeRefon children)") anddocs/PENPOT_SKILL_IMPLEMENTATION_PLAN.md:9. Suggested fix: qualify the sentence to this pass ("not exercised live in this environment; the 2026-09-14 pass in §17 did persist this field contract") and reconcile the §17 vsPENPOT_SKILL_IMPLEMENTATION_PLAN.md:200("live validation of the payload remains") inconsistency.touched/variantId/variantName), and the stub'sButtonroot (extensions/penpot/scripts/live-write-stub.mjs:131) carries none of them, so the fix is exercised only by the pure-function unit test and the stub harness would be green even if the delete were reverted. Suggested fix: addcheck("no clone carries touched", instanceShapes.every((s) => s.touched === undefined))(plusvariantId/variantName) after the root assertions, and give the stub'sBUTTON_ROOT_IDatouched: ["fill-group"]so the strip is exercised end-to-end through the real tool.Low
cloneFill.fillColorRefFile/cloneSpan.typographyRefFileagainstlibraryFileIdinstead of the corresponding value on the main shape, so a component whose colour/typography is owned by a different (e.g. transitive) library file would produce a false FAIL on an otherwise correct clone. Suggested fix: compare againstmainFill.fillColorRefFileandmainSpan.typographyRefFilerespectively (the contract is "preserved", not "points at this file").delete cloned.mainInstancesits inside theisRootbranch, but Penpot'supdate-new-shapestrips:main-instancefrom every clone whenmain-instance?is false (container.cljc:363-364). Currently unreachable (a descendant withmainInstancewould also carrycomponentIdand be refused bydetectNestedInstance), so this is symmetry rather than a live bug. Suggested fix: move thedelete cloned.mainInstanceup next to the otherdelete cloned.*calls (lines 334-343) so the clone contract stays one list, with the root keepingcomponentId/componentFile/componentRoot.Documentation
Updated files: