@invisibleloop/pulse 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,15 +32,21 @@ Use `mcp__chrome-devtools__navigate_page` to load the page route, then `mcp__chr
32
32
 
33
33
  ### 6. Lighthouse — desktop
34
34
 
35
- Run `mcp__chrome-devtools__lighthouse_audit` on the route with `{ "strategy": "desktop" }`.
35
+ **Pre-flight (required before every Lighthouse run):**
36
+ 1. Call `pulse_build` to produce a production build and start the production server on port 3001.
37
+ 2. Call `navigate_page` with `url: "http://localhost:3001/"` so the browser is on the production server.
36
38
 
37
- **Pass bar: Performance, Accessibility, Best Practices, and SEO must all be 100.** Report the actual scores. If any score is below 100, identify the failing audit(s), fix the issue, and restart from step 3.
39
+ Then run `mcp__chrome-devtools__lighthouse_audit` with `{ "device": "desktop" }`.
40
+
41
+ **Pass bar: Accessibility, Best Practices, and SEO must all be 100.** Performance is measured and reported but is not a hard requirement (it varies with machine load). Report the actual scores. If Accessibility, Best Practices, or SEO is below 100, identify the failing audit(s), fix the issue, and restart from step 3.
38
42
 
39
43
  ### 7. Lighthouse — mobile
40
44
 
41
- Run `mcp__chrome-devtools__lighthouse_audit` on the same route with `{ "strategy": "mobile" }`.
45
+ The browser should still be on `http://localhost:3001/` from step 6. Run `mcp__chrome-devtools__lighthouse_audit` with `{ "device": "mobile" }`.
46
+
47
+ **Same pass bar: Accessibility, Best Practices, and SEO must all be 100.** Fix any failures and restart from step 3.
42
48
 
43
- **Same pass bar: all four categories must be 100.** Report the actual scores. If any score is below 100, fix and restart from step 3.
49
+ After both Lighthouse runs pass, call `pulse_restart_server` to return to the dev server.
44
50
 
45
51
  ### 8. Performance
46
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "type": "module",
5
5
  "description": "AI-first frontend framework. The spec is the source of truth.",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- 0.15.0
1
+ 0.15.1
@@ -54,13 +54,9 @@ Use `mcp__chrome-devtools__list_pages` to get all open pages, then call `mcp__ch
54
54
 
55
55
  ### 11. Write verification stamp
56
56
 
57
- Run:
57
+ Call `pulse_stamp`. This writes `.pulse-verified` via the MCP server, which is more reliable than the `date +%s` shell command — the MCP write always lands after all spec edits, avoiding the mtime race condition that can cause the stop hook to block immediately after verification.
58
58
 
59
- ```bash
60
- date +%s > .pulse-verified
61
- ```
62
-
63
- **This must be the last write operation before stopping.** The stop hook compares each edited spec's modification time against this stamp — if any spec is newer than the stamp, the hook blocks. Do not edit any spec file after writing the stamp.
59
+ **This must be the last operation before stopping.** The stop hook compares each edited spec's mtime against this stamp — if any spec is newer than the stamp, the hook blocks. Do not edit any spec file after calling `pulse_stamp`.
64
60
 
65
61
  ### 12. Report
66
62
 
@@ -172,7 +172,7 @@ To add a missing icon: copy a Lucide path (MIT) into `src/ui/icons.js` using `s(
172
172
  | `stat` | `label`, `value`, `change`, `trend` (up/down/neutral), `size` (sm/md/lg — lg for hero KPIs), `center` |
173
173
  | `avatar` | `src`, `alt`, `size` (sm/md/lg/xl), `initials` |
174
174
  | `empty` | `title`, `description`, `action` ({label,href,variant}) |
175
- | `table` | `headers`, `rows` (2D array of HTML strings), `caption` |
175
+ | `table` | `headers`, `rows` (2D array of HTML strings), `caption` | ⚠ Each cell item must be **raw HTML content** — `table()` wraps each item in `<td>` itself. Do NOT pass `<td>...</td>` strings or you will get double-nested cells. |
176
176
  | `spinner` | `size` (sm/md/lg), `color` (accent/muted/white), `label` |
177
177
  | `progress` | `value`, `max` (100), `label`, `showLabel`, `showValue`, `variant`, `size` |
178
178
  | `breadcrumbs` | `items` ([{label,href}] — last item has no href), `separator` |
@@ -83,11 +83,9 @@ Run `mcp__chrome-devtools__list_pages`, then `mcp__chrome-devtools__close_page`
83
83
 
84
84
  ### 12. Write verification stamp
85
85
 
86
- ```bash
87
- date +%s > .pulse-verified
88
- ```
86
+ Call `pulse_stamp`. This writes `.pulse-verified` via the MCP server, which is more reliable than `date +%s > .pulse-verified` — the MCP write always lands after all spec edits, avoiding the mtime race that causes the stop hook to block immediately after verification.
89
87
 
90
- **This must be the last write before stopping.** Do not edit any spec file after writing the stamp.
88
+ **This must be the last operation before stopping.** Do not edit any spec file after calling `pulse_stamp`.
91
89
 
92
90
  ### 13. Report
93
91
 
@@ -298,7 +298,9 @@ Run the tests. Fix every failure.
298
298
 
299
299
  Only invoke `pulse_review` after phases 4–6 all pass. It checks the code for correctness, security, accessibility, DRY violations, and checklist adherence.
300
300
 
301
- > Note: `/verify` includes the review step — if you ran `/verify` and it passed, the Review Agent has already run.
301
+ **Ordering:** `pulse_review` runs **after** Lighthouse, not before. Lighthouse catches contrast failures, missing landmarks, and Best Practices issues that `pulse_review` cannot detect statically. Fixing a Lighthouse failure may require spec edits — so the code review always comes last, against the final version of the code.
302
+
303
+ > Note: `/verify` includes the review step in the correct position — Lighthouse first (steps 6–7), then `pulse_review` (step 10). If you ran `/verify` and it passed, `pulse_review` has already run against the Lighthouse-passing version of the code.
302
304
 
303
305
  ---
304
306
 
package/src/mcp/server.js CHANGED
@@ -18,6 +18,7 @@
18
18
  * pulse_create_page — create a new page spec with proper template
19
19
  * pulse_create_component — create a reusable component
20
20
  * pulse_validate — validate a spec against the schema
21
+ * pulse_stamp — write the .pulse-verified stamp (call as last step of /verify)
21
22
  * pulse_check_version — installed vs static vs npm latest
22
23
  * pulse_update — re-copy pulse-ui assets from package → public/
23
24
  */
@@ -1042,12 +1043,16 @@ ${(() => {
1042
1043
  }
1043
1044
 
1044
1045
  // Check spec source
1045
- // Hex colours in view — exclude anchor hrefs (#id), CSS selector strings, and comments
1046
- // Only flag actual colour values: # followed by exactly 3 or 6 hex chars with no trailing word chars
1046
+ // Hex colours in view — strip anchor hrefs and id attributes before checking so
1047
+ // href="#fixtures" / id="main-content" / href="#join" are never flagged as hex colours.
1048
+ // Only flag actual colour values: # followed by exactly 3 or 6 hex chars.
1047
1049
  const viewBlock = source.slice(source.indexOf('view:'))
1048
- const hexInView = /(?<!href=["']?)#[0-9a-fA-F]{6}\b|(?<!href=["']?)#[0-9a-fA-F]{3}\b(?![0-9a-fA-F])/.test(viewBlock)
1050
+ const viewBlockStripped = viewBlock
1051
+ .replace(/href=["']#[^"']*["']/g, 'href="#"') // href="#anchor" → href="#"
1052
+ .replace(/id=["'][^"']*["']/g, 'id=""') // id="foo" → id=""
1053
+ const hexInView = /#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b(?![0-9a-fA-F])/.test(viewBlockStripped)
1049
1054
  if (hexInView) {
1050
- checks.push('⚠ **Possible hex colour in view** — use var(--ui-*) tokens only (verify: may be an anchor #id href)')
1055
+ checks.push('⚠ **Possible hex colour in view** — use var(--ui-*) tokens only')
1051
1056
  } else {
1052
1057
  checks.push('✓ No obvious hex colours in view')
1053
1058
  }
@@ -1141,12 +1146,39 @@ Fix every issue you find. Then confirm what was changed.
1141
1146
 
1142
1147
  **After confirming fixes: you are back in builder mode. Continue to the verification workflow — navigate to the page in the browser, take a screenshot, run Lighthouse desktop audit, run Lighthouse mobile audit. Do not stop at the review.**`)
1143
1148
 
1144
- // Write the .pulse-verified stamp from the MCP side so the stop hook
1145
- // is never blocked by a sub-second mtime race from a Bash touch command.
1146
- // This is a best-effort write — if it fails, the agent can still Bash-stamp.
1149
+ }
1150
+ )
1151
+
1152
+ // ---------------------------------------------------------------------------
1153
+ // pulse_stamp — write the .pulse-verified stamp
1154
+ // ---------------------------------------------------------------------------
1155
+ // Called as the final step of /verify, after all Lighthouse gates pass and
1156
+ // pulse_review is complete. Using the MCP tool avoids the mtime race that can
1157
+ // occur when `date +%s > .pulse-verified` runs in the same second as the last
1158
+ // spec write, making the stamp appear older than the spec.
1159
+ //
1160
+ // The stop hook compares spec mtimeMs against stamp mtimeMs — not the content.
1161
+ // Writing via MCP ensures the stamp is always written after all spec edits.
1162
+
1163
+ server.registerTool(
1164
+ 'pulse_stamp',
1165
+ {
1166
+ description: `Write the .pulse-verified stamp to clear the stop-hook verification gate.
1167
+
1168
+ Call this as the **last step** of /verify — after Lighthouse desktop + mobile both pass 100/100/100, pulse_review is complete, and no further spec edits will be made.
1169
+
1170
+ The stop hook compares each changed spec's mtime against this stamp. Any spec newer than the stamp blocks the session. Do NOT call this before Lighthouse or before fixing issues found in pulse_review.`,
1171
+ inputSchema: {},
1172
+ },
1173
+ () => {
1174
+ const stampPath = path.join(ROOT, '.pulse-verified')
1175
+ const ts = String(Math.floor(Date.now() / 1000))
1147
1176
  try {
1148
- fs.writeFileSync(path.join(ROOT, '.pulse-verified'), String(Math.floor(Date.now() / 1000)), 'utf8')
1149
- } catch { /* non-critical */ }
1177
+ fs.writeFileSync(stampPath, ts, 'utf8')
1178
+ return text(`✓ .pulse-verified written (${ts}). Stop hook cleared — session can end.`)
1179
+ } catch (err) {
1180
+ return text(`Error writing .pulse-verified: ${err.message}\nFallback: run \`date +%s > .pulse-verified\` in Bash.`)
1181
+ }
1150
1182
  }
1151
1183
  )
1152
1184
 
@@ -2220,21 +2252,24 @@ The extracted values map directly to pulse_intake fields (palette, vibe, styleNo
2220
2252
  ({ source, focus = 'all' }) => {
2221
2253
  const lines = []
2222
2254
  lines.push(`# Inspiration Extraction — ${source}\n`)
2223
- lines.push('Use this template to systematically observe and capture design decisions from the inspiration source.')
2224
- lines.push('Fill in every section you can observe. Leave a field blank if it cannot be determined.\n')
2255
+ lines.push('**You must fill in this template now** — do not return it blank to the user.')
2256
+ lines.push('Use your browsing tool (for URLs) or vision capability (for images) to observe the source and populate every field you can determine.')
2257
+ lines.push('Leave a cell blank only if it is genuinely unobservable. When done, call `pulse_intake` with the extracted values.\n')
2225
2258
  lines.push('---\n')
2226
2259
 
2227
2260
  const isUrl = /^https?:\/\//.test(source) || /\.(com|io|co|app|design|net|org|dev)\b/.test(source)
2228
2261
 
2229
2262
  if (isUrl) {
2230
- lines.push('## Step 1 — Visit the site')
2263
+ lines.push('## Step 1 — Visit the site (do this now)')
2231
2264
  lines.push(`Navigate to: **${source}**`)
2232
2265
  lines.push('Observe the rendered page at desktop width (~1440px) and again at mobile (~375px).')
2233
- lines.push('Take note of the above-the-fold area, then scroll through the full page.\n')
2266
+ lines.push('Take note of the above-the-fold area, then scroll through the full page.')
2267
+ lines.push('**Fill in the sections below based on what you observe. Return a completed extraction, not an empty template.**\n')
2234
2268
  } else {
2235
- lines.push('## Step 1 — Analyse the source')
2269
+ lines.push('## Step 1 — Analyse the source (do this now)')
2236
2270
  lines.push(`Source: **${source}**`)
2237
- lines.push('Use your vision capability or knowledge of this reference to fill in the template below.\n')
2271
+ lines.push('Use your vision capability or knowledge of this reference to fill in the template below.')
2272
+ lines.push('**Return a completed extraction with your observations filled in — not a blank template.**\n')
2238
2273
  }
2239
2274
 
2240
2275
  lines.push('---\n')
@@ -2322,7 +2357,9 @@ The extracted values map directly to pulse_intake fields (palette, vibe, styleNo
2322
2357
  lines.push('})')
2323
2358
  lines.push('```')
2324
2359
  lines.push('')
2325
- lines.push('Then call `pulse_sketch` — pass the layout direction observed above as context in your `brief`.')
2360
+ lines.push('**Now fill in the template above with your observations, then call `pulse_sketch` — pass the layout direction observed above as context in your `brief`.**')
2361
+ lines.push('')
2362
+ lines.push('> Do not return this template blank. The user is waiting for a completed extraction.')
2326
2363
 
2327
2364
  return text(lines.join('\n'))
2328
2365
  }
@@ -2989,15 +3026,21 @@ Run this immediately after writing a theme file — before production build and
2989
3026
  }
2990
3027
 
2991
3028
  // Standard pairings to check — [foreground token, background token, label, isLargeText]
3029
+ // isLargeText=true → WCAG AA large text threshold (3:1); isLargeText=false → body text (4.5:1)
3030
+ // Muted text is checked at both thresholds: body text pairings always need 4.5:1.
3031
+ // When a muted pairing fails 4.5:1 but passes 3:1, the warning distinguishes the two
3032
+ // so designers know it can be used for captions/eyebrows (large) but not body copy (small).
2992
3033
  const PAIRINGS = [
2993
- ['--ui-text', '--ui-bg', 'Body text on page background', false],
2994
- ['--ui-muted', '--ui-bg', 'Muted text on page background', false],
2995
- ['--ui-accent', '--ui-bg', 'Accent text on page background', false],
2996
- ['--ui-text', '--ui-surface', 'Body text on card/surface', false],
2997
- ['--ui-muted', '--ui-surface', 'Muted text on card/surface', false],
2998
- ['--ui-accent-text', '--ui-accent', 'Button text on accent background', false],
2999
- ['--ui-accent', '--ui-surface', 'Accent text on surface', false],
3000
- ['--ui-text', '--ui-surface-2','Text on nested surface', false],
3034
+ ['--ui-text', '--ui-bg', 'Body text on page background', false],
3035
+ ['--ui-muted', '--ui-bg', 'Muted/secondary text on page background (body size)', false],
3036
+ ['--ui-muted', '--ui-bg', 'Muted text on page background (large text / captions)', true],
3037
+ ['--ui-accent', '--ui-bg', 'Accent text on page background', false],
3038
+ ['--ui-text', '--ui-surface', 'Body text on card/surface', false],
3039
+ ['--ui-muted', '--ui-surface', 'Muted text on card/surface (body size)', false],
3040
+ ['--ui-muted', '--ui-surface', 'Muted text on card/surface (large text / captions)', true],
3041
+ ['--ui-accent-text', '--ui-accent', 'Button text on accent background', false],
3042
+ ['--ui-accent', '--ui-surface', 'Accent text on surface', false],
3043
+ ['--ui-text', '--ui-surface-2','Text on nested surface', false],
3001
3044
  ]
3002
3045
 
3003
3046
  const checkSet = (vars, context) => {
@@ -3082,12 +3125,19 @@ Run this immediately after writing a theme file — before production build and
3082
3125
  lines.push(`**${r.label}** — ${r.context}`)
3083
3126
  lines.push(` ${r.fg} on ${r.bg}`)
3084
3127
  lines.push(` Ratio: ${r.ratio}:1 · Needed: ${r.threshold}:1 · Level: ${r.level}`)
3085
-
3128
+
3129
+ // Distinguish between body and large-text failures for --ui-muted
3130
+ if (r.label.includes('large text') || r.label.includes('captions')) {
3131
+ lines.push(` Context: large text / captions (3:1 threshold) — fails even the relaxed threshold.`)
3132
+ } else if (r.label.includes('body size') || r.threshold === 4.5) {
3133
+ lines.push(` Context: body text (4.5:1 threshold) — only use this colour for large text (≥18pt / ≥14pt bold) or decorative elements, not for body copy or captions.`)
3134
+ }
3135
+
3086
3136
  // Suggest a corrected hex value
3087
3137
  if (r.fgHex && r.bgLum !== null) {
3088
3138
  const suggested = suggestContrastFix(r.fgHex, r.bgLum, r.threshold)
3089
3139
  if (suggested) {
3090
- lines.push(` 💡 Suggested fix: use ${suggested} instead of ${r.fgHex}`)
3140
+ lines.push(` Suggested fix: use ${suggested} instead of ${r.fgHex}`)
3091
3141
  }
3092
3142
  }
3093
3143
  lines.push('')
@@ -3105,8 +3155,9 @@ Run this immediately after writing a theme file — before production build and
3105
3155
  if (failures.length > 0) {
3106
3156
  lines.push('---')
3107
3157
  lines.push('**Fix guidance:**')
3158
+ lines.push('• **WCAG AA thresholds:** 4.5:1 for body text (≤18pt regular / ≤14pt bold); 3:1 for large text (≥18pt / ≥14pt bold), UI components, and decorative elements.')
3108
3159
  lines.push('• Mid-tone accents on near-white backgrounds often fail 4.5:1. Darken the accent token or use it only for large text/icons (3:1 threshold).')
3109
- lines.push('• For `--ui-muted` failures: muted text on `--ui-bg` needs at minimum a 4.5:1 ratio. Adjust the muted tone.')
3160
+ lines.push('• For `--ui-muted` "body size" failures: muted text on `--ui-bg` needs 4.5:1 for body copy. If you only use this colour for eyebrows, captions, or large labels (≥18pt), the 3:1 "large text" row is the relevant gate — check whether that row passes.')
3110
3161
  lines.push('• Light-theme `--ui-accent` must be distinctly darker than the page background — many default accent hues are too light.')
3111
3162
  lines.push('• After fixing, run this tool again before `pulse_build`.')
3112
3163
  }
@@ -3410,6 +3461,7 @@ const PULSE_GUIDE_INDEX = `# Pulse Framework Guide
3410
3461
  - \`pulse_create_store\` — register a pulse.store.js you wrote with the Write tool. Write the file first, then call this.
3411
3462
  - \`pulse_create_action\` — generate a correctly-structured action snippet.
3412
3463
  - \`pulse_run_tests\` — run the project test suite (npm test). Use after writing or editing specs.
3464
+ - \`pulse_stamp\` — write the \`.pulse-verified\` stamp. Call as the **last step** of \`/verify\`, after Lighthouse and \`pulse_review\` both pass. Using this MCP tool avoids the mtime race that \`date +%s > .pulse-verified\` can cause when the stamp and the last spec write land in the same filesystem second.
3413
3465
  - \`pulse_fetch_page(url)\` — HTTP GET the dev server URL. Use to verify SSR output.
3414
3466
  - \`pulse_restart_server\` — stop and restart the dev server.
3415
3467
  - \`pulse_build\` — production build + starts prod server on devPort+1 for Lighthouse. Returns the URL. Call \`pulse_restart_server\` after to return to dev. **Slow — takes 30–60 s. Tell the user before calling.**
@@ -150,6 +150,12 @@ try {
150
150
  warnings.push('Inline <style> block detected in view — Pulse\'s CSP (style-src with nonces) will silently block these styles at runtime. Move styles to public/app.css instead.')
151
151
  }
152
152
 
153
+ // Inline <script> blocks in view — blocked by Pulse CSP nonce policy
154
+ // meta.scripts is the correct way to add JS: it receives the server nonce automatically.
155
+ if (/<script[\s>]/i.test(html)) {
156
+ warnings.push('Inline <script> block detected in view — Pulse\'s CSP (script-src with nonces) will block these scripts at runtime. Move JavaScript to a file in public/ and reference it via meta.scripts: [\'/my-script.js\'] in the spec instead.')
157
+ }
158
+
153
159
  // Hardcoded hex colours in view HTML — must use var(--ui-*) tokens instead.
154
160
  // Matches: style="...color:#hex..." or style="...background:#hex..."
155
161
  // Allows: href="#section-id" (anchor links are not colours)