Implement the staged changeset store and the colour/typography primitives with unit tests #194

Closed
opened 2026-09-14 23:10:21 +00:00 by david · 1 comment
Owner

Summary

Implement the staged-changeset store and the two design-token primitives, penpot_create_color and penpot_create_typography. Primitives stage changes rather than writing them, so a whole design system (or a whole screen) can be applied as a single update-file call in a later step.

Background

Depends on: #193

This is the first write-side step, and the architecture it establishes is deliberate: Penpot shape and asset ids are client-generated, so the extension can build a complete change list offline and apply it in one call. update-file applies changes in order, so a parent and its children are created atomically, a rejected batch applies nothing, and the file's revision advances exactly once.

Three pieces are needed:

  1. Staged state. Per target file: the file id, the accumulated changes[], and the base revn observed when staging began. Keyed by file id so two files can be staged independently.
  2. The colour primitive. Change payload confirmed on 2.17: { type: "add-color", color: { id, name, color: "#RRGGBB", opacity } }, which lands at data.colors.<id>. The id is a client-generated UUID.
  3. The typography primitive. Change payload confirmed on 2.17: { type: "add-typography", typography: { id, name, fontId, fontFamily, fontVariantId, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform } }all fields are required, and the server does not validate the font against the team's font list. A typography naming a font the instance does not ship is accepted but will not render as intended, so the tool must resolve fonts deliberately and record substitutions (see below).

Nothing here performs an HTTP write; the commit step does that. Keeping the payload construction pure is what makes the write path testable without a live instance.

Documentation Required

A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting.

docs/reference/penpot-api/

docs/reference/pi-coding-agent/ and docs/reference/typebox/

Implementation Details

extensions/penpot/src/changeset.ts — pure, unit-tested

  • createChangeset(fileId) / stageChange(state, change) / discardChangeset(fileId) / changesetStatus(fileId).
  • State shape: { fileId, revn, changes: Change[], createdAt }, held in a module-level Map<fileId, Changeset>.
  • newId() — client-side UUID generation using crypto.randomUUID() (Node built-in). Every staged asset/shape gets one at stage time.
  • describeChanges(changes) — a human-readable, deterministic preview used by both this milestone's status reporting and the commit preview: one line per change (add-color "flip7-gold" #2BA8A2, add-typography "h2" Inter 24/600).
  • changesetStatus() output: target file, base revn, change count by type, and the preview lines.

extensions/penpot/src/tools/tokens.ts — the two tools

  • penpot_create_color — params file_id, name, color (hex #RRGGBB, or #RRGGBBAA split into hex + opacity), opacity (0–1, optional, default 1). Validate the hex format before staging and reject anything else with a clear message.
  • penpot_create_typography — params file_id, name, font_family (e.g. Inter), font_size (number), font_weight (e.g. 400/600/700), font_style (normal/italic, default normal), line_height (number, default 1.2), letter_spacing (number, default 0), text_transform (default none).
    • Resolve the font against the instance: call get-font-variants for the team and pick the closest available variant; fall back to a documented default when the requested family is absent. Never invent a fontId.
    • Record the substitution in the tool result ("Inter requested → Source Sans Pro used (not installed on this instance)") so the agent can report it rather than silently producing a design that will not render as intended.
  • Both tools reject a file_id that is not the current staged target (or a target with staged changes for a different file) with a clear message — the changeset is per file and mixing files would be a silent data-integrity bug.
  • Both tools return the staged-item preview plus a reminder that nothing is written until commit, and arrange for the later designated-target guard to authorise the file.

Do not add an HTTP write here. Do not add an update-file call.

Acceptance Criteria

  • penpot_create_color stages an add-color change with a client-generated UUID and the exact payload shape {type, color: {id, name, color, opacity}}.
  • penpot_create_typography stages an add-typography change with every required field present and a font id resolved from the instance's font variants.
  • An unknown/invalid hex colour is rejected before staging with a clear message.
  • A font family that the instance does not ship produces a documented substitute plus a recorded substitution note in the tool result.
  • Staged changes accumulate across calls; penpot_discard (or equivalent status/discard tool) clears them and reports what was discarded.
  • Status output shows the target file, base revn, change counts and a deterministic preview.
  • Staging against a second file_id in the same session is handled explicitly (either isolated per file or refused with a clear message) — never silently merged.
  • No HTTP write occurs in either tool; tests assert no update-file call is made.
  • Unit tests cover payload construction, id generation, hex validation, substitution recording and the discard path, with no network access.

Test Plan

node --test extensions/penpot/src/changeset.test.ts extensions/penpot/src/tools/tokens.test.ts

Live sanity check of the read half (which fonts exist), requires PENPOT_URL/PENPOT_TOKEN:

  • Call get-font-variants for the team (via the client or a temporary debug path) and confirm the resolved fontId/fontVariantId values come from that list.
  • Stage three colours and two typographies, then inspect the status output — confirm nothing has been written by re-reading the file and seeing an unchanged revn (the actual commit is validated in the next step).
## Summary Implement the staged-changeset store and the two design-token primitives, `penpot_create_color` and `penpot_create_typography`. Primitives **stage** changes rather than writing them, so a whole design system (or a whole screen) can be applied as a single `update-file` call in a later step. ## Background **Depends on:** #193 This is the first write-side step, and the architecture it establishes is deliberate: Penpot shape and asset ids are **client-generated**, so the extension can build a complete change list offline and apply it in one call. `update-file` applies changes in order, so a parent and its children are created atomically, a rejected batch applies nothing, and the file's revision advances exactly once. Three pieces are needed: 1. **Staged state.** Per target file: the file id, the accumulated `changes[]`, and the base `revn` observed when staging began. Keyed by file id so two files can be staged independently. 2. **The colour primitive.** Change payload confirmed on 2.17: `{ type: "add-color", color: { id, name, color: "#RRGGBB", opacity } }`, which lands at `data.colors.<id>`. The `id` is a client-generated UUID. 3. **The typography primitive.** Change payload confirmed on 2.17: `{ type: "add-typography", typography: { id, name, fontId, fontFamily, fontVariantId, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform } }` — **all fields are required**, and the server does *not* validate the font against the team's font list. A typography naming a font the instance does not ship is accepted but will not render as intended, so the tool must resolve fonts deliberately and record substitutions (see below). Nothing here performs an HTTP write; the commit step does that. Keeping the payload construction pure is what makes the write path testable without a live instance. ## Documentation Required A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting. **`docs/reference/penpot-api/`** - `<PENPOT_URL>/api/main/doc/openapi.json` — the `update-file` command and the change-type union; confirm `add-color` and `add-typography` parameter shapes on this instance. - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/color.cljc — the colour asset record (fields, opacity semantics). - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/typography.cljc — the typography record and which fields the schema requires. - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/font.cljc — font id/family/variant fields. - https://help.penpot.app/user-guide/design-systems/assets/ — product-level meaning of colour and typography assets (swatch naming, text styles), for accurate tool descriptions. - https://help.penpot.app/technical-guide/integration/ — auth and command URL shape for the read calls used to resolve fonts. **`docs/reference/pi-coding-agent/`** and **`docs/reference/typebox/`** - https://pi.dev/docs/latest/extensions — tool registration. - https://github.com/sinclairzx81/typebox — `Type.Object`, `Type.Optional`, `Type.Number`, `Type.String`; how to express a hex-colour pattern and numeric ranges. ## Implementation Details ### `extensions/penpot/src/changeset.ts` — pure, unit-tested - `createChangeset(fileId)` / `stageChange(state, change)` / `discardChangeset(fileId)` / `changesetStatus(fileId)`. - State shape: `{ fileId, revn, changes: Change[], createdAt }`, held in a module-level `Map<fileId, Changeset>`. - `newId()` — client-side UUID generation using `crypto.randomUUID()` (Node built-in). Every staged asset/shape gets one at stage time. - `describeChanges(changes)` — a human-readable, deterministic preview used by both this milestone's status reporting and the commit preview: one line per change (`add-color "flip7-gold" #2BA8A2`, `add-typography "h2" Inter 24/600`). - `changesetStatus()` output: target file, base `revn`, change count by type, and the preview lines. ### `extensions/penpot/src/tools/tokens.ts` — the two tools - **`penpot_create_color`** — params `file_id`, `name`, `color` (hex `#RRGGBB`, or `#RRGGBBAA` split into hex + opacity), `opacity` (0–1, optional, default 1). Validate the hex format before staging and reject anything else with a clear message. - **`penpot_create_typography`** — params `file_id`, `name`, `font_family` (e.g. `Inter`), `font_size` (number), `font_weight` (e.g. `400`/`600`/`700`), `font_style` (`normal`/`italic`, default `normal`), `line_height` (number, default 1.2), `letter_spacing` (number, default 0), `text_transform` (default `none`). - Resolve the font against the instance: call `get-font-variants` for the team and pick the closest available variant; fall back to a documented default when the requested family is absent. Never invent a `fontId`. - **Record the substitution** in the tool result (`"Inter requested → Source Sans Pro used (not installed on this instance)"`) so the agent can report it rather than silently producing a design that will not render as intended. - Both tools reject a `file_id` that is not the current staged target (or a target with staged changes for a different file) with a clear message — the changeset is per file and mixing files would be a silent data-integrity bug. - Both tools return the staged-item preview plus a reminder that nothing is written until commit, and arrange for the later designated-target guard to authorise the file. Do **not** add an HTTP write here. Do not add an `update-file` call. ## Acceptance Criteria - [ ] `penpot_create_color` stages an `add-color` change with a client-generated UUID and the exact payload shape `{type, color: {id, name, color, opacity}}`. - [ ] `penpot_create_typography` stages an `add-typography` change with **every** required field present and a font id resolved from the instance's font variants. - [ ] An unknown/invalid hex colour is rejected before staging with a clear message. - [ ] A font family that the instance does not ship produces a documented substitute plus a recorded substitution note in the tool result. - [ ] Staged changes accumulate across calls; `penpot_discard` (or equivalent status/discard tool) clears them and reports what was discarded. - [ ] Status output shows the target file, base `revn`, change counts and a deterministic preview. - [ ] Staging against a second `file_id` in the same session is handled explicitly (either isolated per file or refused with a clear message) — never silently merged. - [ ] No HTTP write occurs in either tool; tests assert no `update-file` call is made. - [ ] Unit tests cover payload construction, id generation, hex validation, substitution recording and the discard path, with no network access. ## Test Plan ```bash node --test extensions/penpot/src/changeset.test.ts extensions/penpot/src/tools/tokens.test.ts ``` Live sanity check of the *read* half (which fonts exist), requires `PENPOT_URL`/`PENPOT_TOKEN`: - Call `get-font-variants` for the team (via the client or a temporary debug path) and confirm the resolved `fontId`/`fontVariantId` values come from that list. - Stage three colours and two typographies, then inspect the status output — confirm nothing has been written by re-reading the file and seeing an unchanged `revn` (the actual commit is validated in the next step).
david closed this issue 2026-09-15 00:39:05 +00:00
Author
Owner

pi-loop opened and merged a pull request for this issue: #227

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-extensions-and-skills/pulls/227
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-extensions-and-skills#194
No description provided.