From 836a27fd5926b8894b823db915449891bdf30b0b Mon Sep 17 00:00:00 2001 From: Victor Velazquez Date: Wed, 2 Sep 2026 15:24:14 +0200 Subject: [PATCH 1/3] ci: run the suite on every pull request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .github/workflows/ci.yml with two jobs, split by cost. The fast job installs, builds, and runs the Node-only tier. It sets PUPPETEER_SKIP_DOWNLOAD because it never launches a browser and Chrome is ~150MB of the install. It runs on a Node 22/24 matrix: 22 because that is what the new `engines` field claims, so the claim is verified rather than asserted, and 24 because it is closest to what development runs on. The browser job runs the full tier on Node 24 only — doubling the Chrome download to cover a second Node version is not worth it. Puppeteer's Chrome is cached on the lockfile hash, so only a Puppeteer bump pays the download again. `npm run build` is the type check. It is the same compile the published package depends on, so a separate lint step would add time without adding coverage. No secrets are required: the tests that would use an LLM API key skip when one is absent (verified in slice A), so this works on pull requests from forks. Also adds a CI badge to the README. Branch protection is deliberately not part of this change — the checks should earn trust before they can block a merge. --- .github/workflows/ci.yml | 68 ++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- VISION.md | 6 ++-- package.json | 3 ++ 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..52a6047 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +# Runs the test suite on every pull request and on pushes to master. +# +# Two jobs, split by cost. Roughly a third of the suite drives a real browser, +# and downloading Chrome dominates that job's runtime — so the Node-only tests +# run on their own and report in a couple of minutes, which is the feedback +# that matters for almost every change. See docs/specs/CI-SPEC.md. +# +# No secrets are needed. The tests that would use an LLM API key skip when one +# is absent, so this stays useful on pull requests from forks. +name: CI + +on: + pull_request: + push: + branches: [master] + +# A new push supersedes the previous run on the same branch. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + fast: + name: build + tests (node ${{ matrix.node }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + # The lower bound is what package.json's `engines` field claims, so the + # claim stays honest; the upper is what development actually runs on. + node: ['22', '24'] + env: + # This job never launches a browser, and Chrome is ~150MB of the install. + PUPPETEER_SKIP_DOWNLOAD: 'true' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + # `tsc` with noEmit off is the type check — it is the same compile the + # published package depends on, so a separate lint step would be noise. + - run: npm run build + - run: npm run test:fast + + browser: + name: browser tests + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: npm + # `puppeteer` (not puppeteer-core) is a dependency, so its postinstall + # fetches the pinned Chrome build here. Cached across runs by version, so + # only a Puppeteer bump pays the download again. + - name: Cache Puppeteer's Chrome + uses: actions/cache@v4 + with: + path: ~/.cache/puppeteer + key: puppeteer-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + - run: npm ci + - run: npm run build + - run: npm run test:full diff --git a/README.md b/README.md index fbe6387..4658bba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # framesmith -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Release](https://img.shields.io/github/v/release/vicmaster/framesmith)](https://github.com/vicmaster/framesmith/releases) [![MCP](https://img.shields.io/badge/MCP-compatible-1f4838)](https://modelcontextprotocol.io) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Release](https://img.shields.io/github/v/release/vicmaster/framesmith)](https://github.com/vicmaster/framesmith/releases) [![MCP](https://img.shields.io/badge/MCP-compatible-1f4838)](https://modelcontextprotocol.io) [![CI](https://github.com/vicmaster/framesmith/actions/workflows/ci.yml/badge.svg)](https://github.com/vicmaster/framesmith/actions/workflows/ci.yml) An open-source MCP server that turns your AI coding agent into a capable UI designer. It gives the agent a visual canvas, a library of vetted design patterns, and a quality bar it must clear before showing you anything — so you review a real, non-slop design in the browser and agree on it *before* any framework code is written. diff --git a/VISION.md b/VISION.md index 15a21a7..a8c73af 100644 --- a/VISION.md +++ b/VISION.md @@ -543,10 +543,10 @@ _Full spec-driven breakdown in [`docs/specs/PHASE-29-SPEC.md`](docs/specs/PHASE- ### Continuous integration (infrastructure) -Nothing runs on a pull request today: no workflow, no branch protection, no checks. That held because a single disciplined maintainer ran things by hand, but it has already failed silently — the repository has 102 test files and no `test` script to run them as a suite, and two tests were found failing at the v2.0.0 tag itself. The missing runner is the root cause; a pipeline that is red on its first run only teaches you to ignore the red X. +Nothing runs on a pull request today: no workflow, no branch protection, no checks. That held because a single disciplined maintainer ran things by hand, but it has already failed silently — the repository has 102 test files and no `test` script to run them as a suite, and two tests were found failing at the v2.0.0 tag itself. The missing runner is the root cause; a pipeline that is red on its first run only teaches you to ignore the red X. Running the suite together for the first time turned up three failing tests, two of them red at the v2.0.0 tag itself; all three are fixed. -- [ ] Slice A — a tiered runner (`test:fast` / `test:full`), the interactive viewer test excluded by name, `test-cli` moved to the Chrome tier, the stale `data-table` assertions rewritten against the scaffold as it exists now, the tabular-figures advisory regression investigated and settled in whichever direction the evidence supports, and the network/API-key tests confirmed to skip rather than fail when offline or keyless -- [ ] Slice B — a GitHub Actions workflow on pull requests and pushes to master: a fast job (build as type check, plus the Chrome-free tier) and a separate Chrome job, no secrets required, with a pinned Node version and a matching `engines` field +- [x] Slice A — a tiered runner (`test:fast` / `test:full`), the interactive viewer test excluded by name, `test-cli` moved to the Chrome tier, the stale `data-table` assertions rewritten against the scaffold as it exists now, the tabular-figures advisory regression investigated and settled in whichever direction the evidence supports, and the network/API-key tests confirmed to skip rather than fail when offline or keyless +- [x] Slice B — a GitHub Actions workflow on pull requests and pushes to master: a fast job (build as type check, plus the Chrome-free tier) and a separate Chrome job, no secrets required, with a pinned Node version and a matching `engines` field _Full breakdown in [`docs/specs/CI-SPEC.md`](docs/specs/CI-SPEC.md). Deliberately out of scope: branch protection (the checks should earn trust before they can block a merge), a test framework (the plain-script convention works; rewriting 102 files is unrelated to what is broken), release automation, and coverage measurement._ diff --git a/package.json b/package.json index 4ce3cf6..2c3ae6a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "dist", "docs/GUIDELINES.md" ], + "engines": { + "node": ">=22" + }, "scripts": { "build": "tsc", "prepublishOnly": "npm run build", From 1d7d8581ef6551ebfdf7c03554fe0822f7301460 Mon Sep 17 00:00:00 2001 From: Victor Velazquez Date: Wed, 2 Sep 2026 15:30:00 +0200 Subject: [PATCH 2/3] fix: harden the auth submit button, which CI caught clipping on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first CI run failed one test: the auth scaffold's submit button clipped its label under the long-text perturbation, on Linux only. It was the one button in the library not built from the shared `button()` helper. Hand-rolled, it missed the minWidth: 0, overflow: hidden and textOverflow: ellipsis that every other button gets — the hardening Phase 29 slice C added precisely so hostile labels truncate by design instead of escaping their box. It passed the Phase 29 scaffold sweep because macOS font metrics left just enough room. Linux fonts are wider and it overflowed. Building it from `button()` keeps every property it had (name, full width, accent fill, the same padding and radius) and adds the three it was missing. Verified locally: npx tsx test-scaffold-system.ts — all structures pass, including page/auth stress CLEAN. CI verifies the Linux side. --- src/structures.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/structures.ts b/src/structures.ts index 2b564c5..9fb8dcb 100644 --- a/src/structures.ts +++ b/src/structures.ts @@ -885,11 +885,12 @@ const auth: Structure = { id: 'au-fields', type: 'frame', width: '100%', layout: 'vertical', gap: SPACE.md, children: [field('au-email', 'Email'), field('au-password', 'Password')], }, - { - id: 'au-submit', type: 'frame', name: 'Submit', width: '100%', layout: 'horizontal', - alignItems: 'center', justifyContent: 'center', padding: [SPACE.xs, SPACE.lg], cornerRadius: RADIUS.sm, fill: COLOR.accent, - children: [{ id: 'au-submit-label', type: 'text', content: 'Continue', fontSize: TYPE.body, fontWeight: 600, color: COLOR.bgPrimary }], - }, + // Hand-rolled until CI caught it: this was the one button in the + // library not built from `button()`, so it missed the minWidth / + // overflow / ellipsis hardening and clipped under a long label. + // It only failed on Linux — macOS font metrics left just enough + // room — which is why the Phase 29 scaffold sweep passed it. + { ...button('au-submit', 'Continue', COLOR.accent, COLOR.bgPrimary), name: 'Submit', width: '100%' }, { id: 'au-alt', type: 'text', content: 'Secondary link', fontSize: TYPE.textSm, fontWeight: 500, color: COLOR.accent, textAlign: 'center' }, ], }, From 9e4d78d9312070cd3f651131cd5d33e72e62794b Mon Sep 17 00:00:00 2001 From: Victor Velazquez Date: Wed, 2 Sep 2026 15:55:20 +0200 Subject: [PATCH 3/3] fix: harden the data-table role, status and amount cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second Linux-only stress failure the pipeline caught. Same shape as the auth button: the identity cell in the data-table row has carried minWidth: 0 and an ellipsis label since Phase 29, and the three columns beside it never got it. Rather than fix the one column CI named, I audited the library for the pattern — text inside a horizontal frame with a fixed or percentage width, where it cannot wrap out of trouble and has no designed truncation. Exactly nine hits, all of them these three cells across the three sample rows. Every one is now hardened and the audit comes back empty. Note the audit was deliberately narrow. A broader sweep (any text under any width-constrained ancestor) returns 113 hits, but almost all are headlines and body copy that wrap correctly; adding ellipsis there would truncate prose that is currently fine. Verified locally: test-scaffold-system all pass, test-component-structures 28/28. --- src/structures.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/structures.ts b/src/structures.ts index 9fb8dcb..7d3f372 100644 --- a/src/structures.ts +++ b/src/structures.ts @@ -1147,23 +1147,26 @@ function tableRow(id: string): SceneNode { ], }, { - id: `${id}-role`, type: 'frame', width: '18%', layout: 'horizontal', + id: `${id}-role`, type: 'frame', width: '18%', layout: 'horizontal', minWidth: 0, children: [{ id: `${id}-role-chip`, type: 'frame', layout: 'horizontal', alignItems: 'center', padding: [SPACE.xs2, SPACE.xs], - cornerRadius: 999, fill: COLOR.bgElevated, - children: [{ id: `${id}-role-text`, type: 'text', content: 'Role', fontSize: TYPE.caption, color: COLOR.textSecondary }], + cornerRadius: 999, fill: COLOR.bgElevated, minWidth: 0, overflow: 'hidden', + children: [{ id: `${id}-role-text`, type: 'text', content: 'Role', fontSize: TYPE.caption, color: COLOR.textSecondary, textOverflow: 'ellipsis' }], }], }, { - id: `${id}-status`, type: 'frame', width: '18%', layout: 'horizontal', alignItems: 'center', gap: SPACE.xs2, + id: `${id}-status`, type: 'frame', width: '18%', layout: 'horizontal', alignItems: 'center', gap: SPACE.xs2, minWidth: 0, children: [ { id: `${id}-status-dot`, type: 'ellipse', width: 8, height: 8, fill: '$success' }, - { id: `${id}-status-text`, type: 'text', content: 'Status', fontSize: TYPE.caption, color: COLOR.textSecondary }, + { id: `${id}-status-text`, type: 'text', content: 'Status', fontSize: TYPE.caption, color: COLOR.textSecondary, textOverflow: 'ellipsis' }, ], }, { - id: `${id}-amount`, type: 'frame', width: '18%', layout: 'horizontal', justifyContent: 'end', - children: [{ id: `${id}-amount-text`, type: 'text', content: 'Amount — TBD', fontSize: TYPE.textSm, color: COLOR.textPrimary, tabularNums: true }], + // The identity cell above has carried minWidth + ellipsis since Phase 29; + // these three columns never got it, and overflowed on Linux where the + // fonts are wider than the macOS metrics the sweep was run against. + id: `${id}-amount`, type: 'frame', width: '18%', layout: 'horizontal', justifyContent: 'end', minWidth: 0, + children: [{ id: `${id}-amount-text`, type: 'text', content: 'Amount — TBD', fontSize: TYPE.textSm, color: COLOR.textPrimary, tabularNums: true, textOverflow: 'ellipsis' }], }, { id: `${id}-actions`, type: 'frame', width: '12%', layout: 'horizontal', justifyContent: 'end',