issue-194: Implement the staged changeset store and the colour/typography primitives with unit tests #227
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!227
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/issue-194/implement-the-staged-changeset-store-and-the-colou"
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?
194: Implement the staged changeset store and the colour/typography primitives with unit tests
Closes #194 — 194.
Generated in 13m 58s.
Review findings
Now let me apply the fixes. Starting with
changeset.ts:Nowtokens.ts— constraintext_transform, enforce non-negativerevn, and reorder the typography fetch:Nowindex.tsand its test:Now the CHANGELOG entry:Now add the regression tests. Firstchangeset.test.ts:Nowtokens.test.ts— opacity-zero boundary tests and the phantom-changeset regression:Now the live harness. Let me check the Node version and add theget-font-variantsread:Now let me run the penpot tests:# Review Findings — 194Summary
Resolved the High changelog finding and every Medium/Low finding except the designated-target semantics decision (
extensions/penpot/src/tools/tokens.ts:278), which belongs with thepenpot_commitguard and needs a scope call on what "a file explicitly named by the user" means. Fixes applied: added theCHANGELOG.md### Addedbullet; maderesolveFonttreat the built-in Source Sans Pro as always installed (no false substitution); fetched font variants before creating the changeset so a failedpenpot_create_typographyleaves no phantom entry; registered asession_shutdownreset (withfakePi.onand a wiring test); madediscardChangesettreat a zero-change changeset as nothing-to-discard; corrected the module header; constrainedtext_transformto Penpot's enum; bounded the invalid-colour excerpt via the sharedtruncateExcerpt; enforced non-negativerevn; added the opacity-0??-chain tests and the phantom-changeset regression; and addedget-font-variantsto the live harness and the findings command table. Gates:npm test→ 304 pass / 1 fail, the same pre-existingscripts/local-install.test.mjs:1875mongodb-manifest assertion (package.json untouched by this diff). No build/lint/type-check command exists in this repo, so onlynpm testwas runnable.Critical
High
CHANGELOG.md:9— no entry was added for the two new staging tools +penpot_discardunder## [Unreleased]→### Added. Every prior penpot issue in this repo added a changelog bullet in the same commit (git log -- CHANGELOG.md: 189, 190, 191, 192, 193), and this is a user-facing feature; the docs-sync stage only coversREADME.mdanddocs/**/*.md, so it will not repair this. Suggested fix: add an### Addedbullet describing the stagedpenpot_create_color/penpot_create_typography/penpot_discardtools, the per-file changeset, hex validation and the font-substitution behaviour.Medium
extensions/penpot/src/changeset.ts:409— requesting the built-in family itself is reported as a substitution.get-font-variantsnever returns the built-in Source Sans Pro, somatchesis empty forfamily: "Source Sans Pro"/"sourcesanspro"and the code takes the fallback branch, emitting a false claim. Repro (verified):resolveFont({family:"Source Sans Pro",weight:600}, [])→substitution: {requestedFamily:"Source Sans Pro", usedFamily:"Source Sans Pro", reason:"not installed on this instance"}, and the tool text saysSubstitution: Source Sans Pro requested → Source Sans Pro used (not installed on this instance). Suggested fix: treat the built-in family aliases as always installed (matchrequest.familyagainstSOURCE_SANS_PRO_VARIANTS/DEFAULT_FONT_DISPLAY_NAMEbefore the fallback branch, or omitsubstitutionwhenfamilyKey(request.family)equals the resolved family key).extensions/penpot/src/tools/tokens.ts:406— a failedpenpot_create_typographyleaves a phantom changeset behind.ensureChangeset()(line 405) creates and authorises the changeset before the falliblefetchInstalledFonts()(line 406); if that fetch fails the store keeps an entry for the file withchanges: []. BecauseactiveChangesetFileId()only returns files withchanges.length > 0, a different file can then be staged, and the original file is afterwards refused with "file [A] cannot be staged: file [B] already has staged changes" even though nothing was ever staged for A. Verified by repro: stage typography for A with a failingget-font-variants, then stage a colour for B (allowed), then stage A again (refused). Suggested fix: fetch the font variants (and validate everything fallible) beforeensureChangeset/authorizeTarget, or make the guard consultgetChangeset(fileId) !== undefinedinstead of only non-empty changesets and clean up a created-but-empty changeset when the call throws.extensions/penpot/src/changeset.ts:455— the module-level store andauthorizedTargetsset have no session lifecycle hook, so staged changes and write authorisations survive into a new session in the same pi process (extension factories are cached per process by pi's loader, which only re-runs theindex.tsfactory — module state is not reset). This contradicts the module's own "deliberate session state" framing and silently widens the future designated-target guard. Suggested fix: inextensions/penpot/src/index.tsregisterpi.on("session_shutdown", () => resetChangesets())(and extendfakePiinindex.test.tswithon), or expose aresetChangesets()call from the entry point.extensions/penpot/src/tools/tokens.ts:278—authorizeTarget(fileId)is called fromensureChangesetfor any file id the model passes, so "authorised by construction" means "the model named a file it can read". The design (docs/PENPOT_SKILL_DESIGN.md§9 / plan M2) requires writes only to files the extension created in a nominated project or a file explicitly named by the user; a guard built on this set cannot distinguish those, so it will authorise an arbitrary existing file. Suggested fix: decide the designated-target semantics before thepenpot_commitguard lands — e.g. record only files created viacreate-fileplus targets the user named, and require an explicit user-supplied confirmation flag rather than auto-authorising on staging.Low
extensions/penpot/src/tools/tokens.ts:513— an empty changeset is reported as a discard:details.discardedistrueanddetails.status.changeCountis0, while the text saysNo staged changes for file [..] — nothing to discard.(verified by repro; reachable via the phantom-changeset path above). Suggested fix: treat a zero-change changeset asdiscarded: false(or drop the empty changeset and reportnothing to discardconsistently) sodetails.discardedand the text agree.extensions/penpot/src/changeset.ts:15— the module header says one changeset per file means "two files can be staged independently and a whole design system can be previewed before anything is written", butassertSingleActiveTargetrefuses a second file andextensions/penpot/README.mddocuments the refusal. Suggested fix: correct the header to state the store is per-file while the tools enforce a single active target (or relax the guard if multi-file staging was intended).extensions/penpot/src/tools/tokens.ts:158—text_transformis an unconstrainedType.String()while the sibling fields are validated (font_styleis a union, opacity has min/max, the hex is validated). The server expectsnone | uppercase | lowercase | capitalize; an arbitrary string is staged verbatim. Suggested fix:Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("uppercase"), Type.Literal("lowercase"), Type.Literal("capitalize")])), matchingDEFAULT_TEXT_TRANSFORM.extensions/penpot/src/changeset.ts:180— the invalid-colour error interpolates the unbounded raw input (invalid colour "${raw}" …), unlike the client/errors modules which cap excerpts (MAX_RAW_EXCERPT_CHARSinerrors.ts). A model-supplied long string (e.g. a pasted data URI) floods the context. Suggested fix: bound it, e.g.invalid colour "${truncateExcerpt(raw, 40)}".extensions/penpot/src/tools/tokens.ts:226—readRevn's doc says "A finite, non-negative number" but the implementation accepts any finite number, including negatives. Suggested fix: enforcerevn >= 0in the predicate or amend the comment.extensions/penpot/src/tools/tokens.ts:372— no test covers the??-chain boundary where a falsy-but-valid opacity must win:#RRGGBB00(alpha 0) and an explicitopacity: 0. The chain is correct today, but it is exactly the kind of??vs||case a later edit breaks. Suggested fix: add a tool-level test stagingcolor: "#2BA8A200"and one withopacity: 0assertingchange.color.opacity === 0.extensions/penpot/scripts/live-read-check.mjs:63— the newget-font-variantsread isn't in the committed live harness or theextensions/penpot/findings.mdcommand table, although the repo's convention (issue 193) is a live smoke check per command; the non-empty variant row shape is only unit-stubbed. I verified field names (fontId/fontFamily/fontWeight/fontStyle, camelCased bysm/json-transformer) and thecustom-<uuid>/<style>-<weight>derivation against Penpot 2.17.2 source, so the risk is low. Suggested fix: add the command tolive-read-check.mjs(and the findings table) once a token is available.Documentation
Updated files: