@invisibleloop/pulse 0.14.1 → 0.15.0

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.
Files changed (46) hide show
  1. package/.claude/commands/build-page.md +5 -1
  2. package/.claude/commands/verify.md +23 -13
  3. package/docs/.claude/pulse-checklist.md +5 -3
  4. package/docs/public/.pulse-ui-version +1 -1
  5. package/docs/public/docs.css +236 -0
  6. package/docs/public/home-brut.css +998 -0
  7. package/docs/public/menu.js +237 -6
  8. package/docs/public/pulse-ui.css +44 -0
  9. package/docs/public/theme.css +8 -0
  10. package/docs/server.js +24 -0
  11. package/docs/src/lib/layout.js +51 -3
  12. package/docs/src/pages/getting-started.js +1 -1
  13. package/docs/src/pages/home.js +231 -312
  14. package/docs/src/pages/how-it-works.js +67 -12
  15. package/examples/.claude/commands/pulse-build.md +7 -0
  16. package/examples/.claude/commands/pulse-dev.md +7 -0
  17. package/examples/.claude/commands/pulse-start.md +9 -0
  18. package/examples/.claude/commands/pulse-stop.md +5 -0
  19. package/examples/.claude/commands/verify.md +84 -0
  20. package/examples/.claude/coverage-check.js +127 -0
  21. package/examples/.claude/pulse-checklist.md +177 -0
  22. package/examples/public/.pulse-ui-version +1 -0
  23. package/examples/public/pulse-ui.css +4031 -0
  24. package/examples/public/pulse-ui.js +257 -0
  25. package/package.json +1 -1
  26. package/public/.pulse-ui-version +1 -1
  27. package/public/pulse-ui.css +44 -0
  28. package/scripts/build.js +127 -21
  29. package/src/agent/checklist.md +5 -3
  30. package/src/agent/guide-components.md +2 -2
  31. package/src/agent/guide-examples.md +310 -32
  32. package/src/agent/guide-styles.md +15 -0
  33. package/src/agent/guide-templates.md +3 -0
  34. package/src/agent/identity.md +2 -1
  35. package/src/agent/skills/verify/SKILL.md +64 -28
  36. package/src/agent/workflow.md +53 -17
  37. package/src/cli/dev.js +49 -13
  38. package/src/cli/index.js +4 -10
  39. package/src/cli/logger.js +145 -0
  40. package/src/cli/scaffold.js +55 -1
  41. package/src/mcp/server.js +174 -26
  42. package/src/mcp/validate-worker.js +21 -6
  43. package/src/server/index.js +20 -16
  44. package/src/spec/schema.js +19 -3
  45. package/src/spec/schema.test.js +28 -0
  46. package/src/ui/button.js +1 -1
@@ -87,13 +87,20 @@ export default {
87
87
  <p>Together these replace the need to paste documentation into a system prompt or rely on the agent's training data to know how Pulse works.</p>
88
88
 
89
89
  ${section('resources', 'Resources: what the agent knows')}
90
- <p>The MCP server exposes two resources the agent reads before doing any work:</p>
91
- <ul>
92
- <li><strong><code>pulse://guide</code></strong> — the complete framework reference, split into focused sections so each fits comfortably in a single read. The agent fetches whichever sections are relevant to the task at hand.</li>
93
- <li><strong><code>pulse://persona</code></strong> — the quality bar the agent holds itself to: always write correct SSR, always handle errors, never skip empty states, never hardcode colours, never use <code>data-event</code> on text inputs. The persona defines what "done" means.</li>
94
- </ul>
90
+ <p>The MCP server exposes read-only documents the agent fetches at any time. There are two top-level resources and a set of focused guide sections:</p>
91
+
92
+ <dl class="definition-list">
93
+ <dt><code>pulse://quickstart</code></dt>
94
+ <dd>Workflow phases, spec skeleton, component rules, and theming essentials combined into a single fetch. Used for targeted edits, bug fixes, or simple one-shot page builds — avoids loading the full guide for small tasks.</dd>
95
+
96
+ <dt><code>pulse://workflow</code></dt>
97
+ <dd>The full build workflow — every phase, pass gate, and ordering rule. The agent reads this before starting any new page or project.</dd>
98
+
99
+ <dt><code>pulse://persona</code></dt>
100
+ <dd>The quality bar the agent holds itself to: always write correct SSR, always handle errors, never skip empty states, never hardcode colours, never use <code>data-event</code> on text inputs. The persona defines what "done" means.</dd>
101
+ </dl>
95
102
 
96
- <p>The guide sections are:</p>
103
+ <p>The guide is split into topic sections the agent fetches as needed:</p>
97
104
  <dl class="definition-list">
98
105
  <dt><code>pulse://guide/spec</code></dt>
99
106
  <dd>Spec format — state, mutations, actions, streaming SSR, validation, key rules, and form layout.</dd>
@@ -112,12 +119,51 @@ export default {
112
119
 
113
120
  <dt><code>pulse://guide/examples</code></dt>
114
121
  <dd>Complete working page examples covering common patterns.</dd>
122
+
123
+ <dt><code>pulse://guide/templates</code></dt>
124
+ <dd>Pre-built landing page templates, adaptation rules, and theme CSS patterns. Fetched when building a new branded site or landing page.</dd>
125
+
126
+ <dt><code>pulse://guide/design-references</code></dt>
127
+ <dd>Twelve design directions with vibes, component combinations, palette patterns, and signature moves. Fetched during intake to pick the right aesthetic rather than defaulting to a generic SaaS look.</dd>
128
+
129
+ <dt><code>pulse://guide/design-gallery</code></dt>
130
+ <dd>All templates with visual descriptions and key components. Includes critical prop-name references and component recipes for cards, stat strips, and forms. Fetched when adapting a template or combining components.</dd>
131
+
132
+ <dt><code>pulse://guide/explore</code></dt>
133
+ <dd>Zone-based layout thinking, seven structural gestures, raw HTML patterns without components, and an anti-pattern checklist. Fetched when a distinctive or unusual layout is needed.</dd>
115
134
  </dl>
116
135
 
117
136
  ${callout('note', 'The guide is authoritative. When there is a question about how something should be structured — where state lives, how validation is wired, how streaming works — the guide answers it. The agent does not guess.')}
118
137
 
119
138
  ${section('tools', 'Tools: what the agent can do')}
120
- <p>The MCP server provides tools across four categories:</p>
139
+ <p>The MCP server provides tools across five categories:</p>
140
+
141
+ <h3 class="doc-h3">Design &amp; intake</h3>
142
+ <dl class="definition-list">
143
+ <dt><code>pulse_extract_inspiration</code></dt>
144
+ <dd>Extracts a structured design brief from a URL or image shared as inspiration. Called before <code>pulse_intake</code> when the user says "I like the look of X" — it tells the agent exactly what to observe and capture.</dd>
145
+
146
+ <dt><code>pulse_intake</code></dt>
147
+ <dd>Captures the product name, pitch, features, palette, vibe, and anti-style before scaffolding any new page or branded template. Returns a structured brief with copy-ready content, early contrast warnings, and component suggestions. Always runs before <code>pulse_sketch</code>.</dd>
148
+
149
+ <dt><code>pulse_sketch</code></dt>
150
+ <dd>Generates three structurally distinct layout directions (full-bleed, asymmetric split, editorial flow, etc.) before any code is written. Called after <code>pulse_intake</code> — prevents every project from defaulting to the same centred-hero layout.</dd>
151
+
152
+ <dt><code>pulse_intent</code></dt>
153
+ <dd>Intent engine — describe what you want to build and get back a matched archetype, recommended components, a ready-to-adapt spec scaffold, and which guide sections to read first. An alternative entry point to <code>pulse://workflow</code> for targeted tasks.</dd>
154
+
155
+ <dt><code>pulse_design_review</code></dt>
156
+ <dd>Reviews the built page's visual design against the original product brief from <code>pulse_intake</code>. Checks whether the design looks and feels appropriate for the product, audience, and vibe. Runs after screenshot, before Lighthouse.</dd>
157
+
158
+ <dt><code>pulse_layout_review</code></dt>
159
+ <dd>Multi-viewport layout review — checks for overflow, collapsed sections, and broken images at different screen widths. Runs after design approval but before Lighthouse, catching layout issues that Lighthouse doesn't flag.</dd>
160
+
161
+ <dt><code>pulse_tokens</code></dt>
162
+ <dd>Lists all available <code>--ui-*</code> CSS custom property tokens from the installed <code>pulse-ui.css</code>. Used before writing theme overrides to avoid guessing token names.</dd>
163
+
164
+ <dt><code>pulse_check_contrast</code></dt>
165
+ <dd>Static WCAG contrast checker. Extracts colour variable definitions from a theme file and checks common foreground/background pairings for WCAG AA compliance. Run immediately after writing a theme file to catch palette mistakes before Lighthouse.</dd>
166
+ </dl>
121
167
 
122
168
  <h3 class="doc-h3">Scaffolding</h3>
123
169
  <dl class="definition-list">
@@ -136,11 +182,17 @@ export default {
136
182
 
137
183
  <h3 class="doc-h3">Inspection</h3>
138
184
  <dl class="definition-list">
185
+ <dt><code>pulse_status</code></dt>
186
+ <dd>Project health snapshot — pages, routes, server status, last build. Called at the start of a session to orient quickly without reading files individually.</dd>
187
+
139
188
  <dt><code>pulse_list_structure</code></dt>
140
189
  <dd>Lists every page, component, and store that already exists in the project. The agent calls this before creating anything — to avoid duplication and to understand what the codebase already provides.</dd>
141
190
 
142
191
  <dt><code>pulse_fetch_page</code></dt>
143
192
  <dd>Reads the full source of an existing spec file. Used when the agent needs to understand an existing page before editing it, or when a review of the current state is needed.</dd>
193
+
194
+ <dt><code>pulse_list_icons</code></dt>
195
+ <dd>Lists all available Pulse UI icon names, grouped by category. Called before importing icons — the agent never guesses a name.</dd>
144
196
  </dl>
145
197
 
146
198
  <h3 class="doc-h3">Validation &amp; review</h3>
@@ -148,26 +200,29 @@ export default {
148
200
  <dt><code>pulse_validate</code></dt>
149
201
  <dd>Validates a spec against the Pulse schema. Returns errors (which block progress) and warnings (which must also be resolved). The agent calls this after writing or editing every spec file.</dd>
150
202
 
203
+ <dt><code>pulse_suggest</code></dt>
204
+ <dd>Draft-mode feedback — paste a partial or complete spec and get constructive suggestions before running the hard validator. Unlike <code>pulse_validate</code>, it is collaborative rather than a gate: it notices patterns, spots likely omissions, and offers ideas. Used mid-build for a second opinion.</dd>
205
+
151
206
  <dt><code>pulse_review</code></dt>
152
207
  <dd>Switches the agent into reviewer mode. Returns the spec source, the rendered HTML output, and a structured checklist covering accessibility, empty states, error handling, component usage, security, and correctness. The agent reads its own output critically and fixes every issue before continuing.</dd>
208
+
209
+ <dt><code>pulse_run_tests</code></dt>
210
+ <dd>Runs the project test suite (<code>npm test</code>) and returns the full output. Called after writing or editing specs to verify nothing is broken.</dd>
153
211
  </dl>
154
212
 
155
- <h3 class="doc-h3">Server</h3>
213
+ <h3 class="doc-h3">Server &amp; maintenance</h3>
156
214
  <dl class="definition-list">
157
215
  <dt><code>pulse_restart_server</code></dt>
158
216
  <dd>Restarts the Pulse dev server. Called after structural changes — adding a new page, modifying the server entry — to reload all specs and re-register routes.</dd>
159
217
 
160
218
  <dt><code>pulse_build</code></dt>
161
219
  <dd>Builds the project for production. Bundles specs, hashes assets, and writes the manifest. Called when the agent needs to verify the production build or prepare a release.</dd>
162
- </dl>
163
220
 
164
- <h3 class="doc-h3">Maintenance</h3>
165
- <dl class="definition-list">
166
221
  <dt><code>pulse_check_version</code></dt>
167
222
  <dd>Reports the installed Pulse version and whether an update is available.</dd>
168
223
 
169
224
  <dt><code>pulse_update</code></dt>
170
- <dd>Updates the Pulse package to the latest version.</dd>
225
+ <dd>Updates the Pulse package to the latest version and copies the latest UI assets and agent files into the project.</dd>
171
226
  </dl>
172
227
 
173
228
  ${section('flow', 'What happens when you ask for something')}
@@ -0,0 +1,7 @@
1
+ Build this Pulse project for production.
2
+
3
+ ```bash
4
+ pulse build
5
+ ```
6
+
7
+ This bundles all pages into `public/dist/`. When complete, confirm the build succeeded and list the generated files.
@@ -0,0 +1,7 @@
1
+ Start (or restart) the Pulse dev server for this project.
2
+
3
+ ```bash
4
+ pulse stop; pulse dev
5
+ ```
6
+
7
+ The server port is read from `pulse.config.js` (defaults to 3000).
@@ -0,0 +1,9 @@
1
+ Start the Pulse production server for this project.
2
+
3
+ First ensure a build exists (`public/dist/`). Then run:
4
+
5
+ ```bash
6
+ pulse start
7
+ ```
8
+
9
+ The production server uses the port from `pulse.config.js` (defaults to 3000).
@@ -0,0 +1,5 @@
1
+ Stop the Pulse dev server for this project.
2
+
3
+ ```bash
4
+ pulse stop
5
+ ```
@@ -0,0 +1,84 @@
1
+ # Verify
2
+
3
+ Run the full verification loop on the current page or the page specified in $ARGUMENTS.
4
+
5
+ ## Steps
6
+
7
+ ### 1. Identify the target
8
+
9
+ If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify the most recently edited spec file from context.
10
+
11
+ ### 2. Validate the spec
12
+
13
+ Use `pulse_validate` with the spec file content. If validation fails, report every error clearly and stop — do not proceed to screenshot until the spec is valid.
14
+
15
+ ### 3. Check the dev server
16
+
17
+ Use `pulse_fetch_page` to confirm the server is responding for the route. If it errors, use `pulse_restart_server` and retry once.
18
+
19
+ **If `pulse_fetch_page` returns unexpected HTML (e.g. a different project's page title, a Next.js 404, or any non-Pulse response):** check the `X-Pulse` response header — Pulse servers always send this. If it is absent, the wrong server is running on that port. Stop and inform the user before continuing.
20
+
21
+ ### 4. Screenshot
22
+
23
+ Use `mcp__chrome-devtools__navigate_page` to load the page route, then `mcp__chrome-devtools__take_screenshot` to capture the result. Describe what you see — layout, content, any obvious rendering issues.
24
+
25
+ ### 5. Lighthouse — desktop
26
+
27
+ Run `mcp__chrome-devtools__lighthouse_audit` on the route with `{ "strategy": "desktop" }`.
28
+
29
+ **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 2.
30
+
31
+ ### 6. Lighthouse — mobile
32
+
33
+ Run `mcp__chrome-devtools__lighthouse_audit` on the same route with `{ "strategy": "mobile" }`.
34
+
35
+ **Same pass bar: Accessibility, Best Practices, and SEO must all be 100.** Fix any failures and restart from step 2.
36
+
37
+ **If you need to visually inspect the layout at a mobile viewport width** (e.g. to debug a wrapping or overflow issue), use `mcp__chrome-devtools__emulate` with `viewport: "390x844x2,mobile,touch"` — not `resize_page`. The `emulate` tool reliably sets the viewport; `resize_page` does not reliably trigger responsive layout changes. Reset afterward with `emulate` using the desktop viewport: `"1440x900x1"`.
38
+
39
+ ### 7. Performance
40
+
41
+ Navigate to the page route, then run `mcp__chrome-devtools__performance_start_trace` with `reload: true` and `autoStop: true`. Report LCP and CLS. Flag any LCP insight that suggests a fixable problem (render-blocking resources, large image load delay, etc.).
42
+
43
+ ### 8. Console errors
44
+
45
+ Use `mcp__chrome-devtools__list_console_messages` — report any errors or unexpected warnings.
46
+
47
+ ### 9. Code review
48
+
49
+ Call `pulse_review` with the absolute path to the spec file. Read the source, rendered HTML, validator output, and checklist it returns. Fix every issue before proceeding. This is the structured review — do not skip it or replace it with a manual checklist read.
50
+
51
+ ### 10. Close the browser
52
+
53
+ Use `mcp__chrome-devtools__list_pages` to get all open pages, then call `mcp__chrome-devtools__close_page` for every page ID returned. This closes the entire browser, not just the tab. `pageId` must be a number, not a string.
54
+
55
+ ### 11. Write verification stamp
56
+
57
+ Run:
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.
64
+
65
+ ### 12. Report
66
+
67
+ Summarise:
68
+ - Validation: pass or fail (with errors if any)
69
+ - Visual: what was visible in the screenshot
70
+ - Lighthouse desktop: scores for all four categories
71
+ - Lighthouse mobile: scores for all four categories
72
+ - Performance: LCP and CLS values, any flagged insights
73
+ - Console: any errors
74
+ - Review: pass or issues found and fixed
75
+
76
+ **Pass bar — all of these must be met before the page is confirmed good:**
77
+ - Validation: clean (no errors, no warnings)
78
+ - Lighthouse desktop: Accessibility, Best Practices, SEO all 100
79
+ - Lighthouse mobile: same
80
+ - CLS: 0.00 — any layout shift is a blocker
81
+ - LCP: report the value; flag and fix any insight that identifies a fixable cause
82
+ - Console: no errors
83
+
84
+ If any gate fails, fix the issue and run `/verify` again.
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * coverage-check.js
4
+ *
5
+ * Called by the Stop hook in .claude/settings.json to enforce test coverage.
6
+ * Runs `npm run test:coverage` and blocks the agent if any src/pages/ file
7
+ * has uncovered lines — except async action run() and server fetcher functions
8
+ * which cannot be tested without mocking real APIs.
9
+ *
10
+ * Outputs a Claude Code hook JSON decision to stdout.
11
+ */
12
+
13
+ import { execSync } from 'child_process'
14
+ import fs from 'fs'
15
+
16
+ // No test files → nothing to check
17
+ const hasTests = fs.existsSync('src/pages') && (function scan(d) {
18
+ for (const f of fs.readdirSync(d)) {
19
+ const p = `${d}/${f}`
20
+ if (fs.statSync(p).isDirectory()) { if (scan(p)) return true }
21
+ else if (f.endsWith('.test.js')) return true
22
+ }
23
+ return false
24
+ })('src/pages')
25
+
26
+ if (!hasTests) process.exit(0)
27
+
28
+ let out = ''
29
+ try {
30
+ out = execSync('npm run test:coverage 2>&1', { encoding: 'utf8', timeout: 60000 })
31
+ } catch (e) {
32
+ out = e.stdout || e.message
33
+ }
34
+
35
+ // Parse the coverage report — look for lines with uncovered line numbers
36
+ const lines = out.split('\n')
37
+ let inReport = false
38
+ const rawGaps = []
39
+
40
+ for (const line of lines) {
41
+ if (line.includes('start of coverage report')) { inReport = true; continue }
42
+ if (!inReport) continue
43
+
44
+ // Match: # filename.js | line% | branch% | func% | 30-36 50
45
+ const m = line.match(/^[#ℹ]\s+([\w./[\]-]+\.js)\s*\|\s*[\d.]+\s*\|\s*[\d.]+\s*\|\s*[\d.]+\s*\|\s*([^|\n]+)$/)
46
+ if (m) {
47
+ const uncov = m[2].trim()
48
+ if (uncov && uncov !== 'uncovered lines') {
49
+ rawGaps.push({ file: m[1].trim(), uncovered: uncov })
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Returns true if all uncovered line numbers in the given file fall inside
56
+ * a `server:` property block or an action `run:` function — both of which
57
+ * call real external APIs and are explicitly exempt from coverage requirements.
58
+ */
59
+ function allLinesExempt(filePath, uncoveredSpec) {
60
+ let src
61
+ try { src = fs.readFileSync(filePath, 'utf8') } catch { return false }
62
+ const srcLines = src.split('\n')
63
+
64
+ // Expand "30-36 50" → [30, 31, 32, 33, 34, 35, 36, 50]
65
+ const lineNums = []
66
+ for (const part of uncoveredSpec.split(/\s+/)) {
67
+ const range = part.match(/^(\d+)-(\d+)$/)
68
+ if (range) {
69
+ for (let i = parseInt(range[1]); i <= parseInt(range[2]); i++) lineNums.push(i)
70
+ } else if (/^\d+$/.test(part)) {
71
+ lineNums.push(parseInt(part))
72
+ }
73
+ }
74
+
75
+ // Build a set of exempt line numbers: lines inside `server:` blocks or action `run:` functions.
76
+ // Strategy: scan the source tracking brace depth relative to the start of each exempt block.
77
+ const exemptLines = new Set()
78
+ let i = 0
79
+ while (i < srcLines.length) {
80
+ const trimmed = srcLines[i].trimStart()
81
+ // Detect start of `server:` property or action `run:` function
82
+ const isExemptStart = /^server\s*:/.test(trimmed) || /^run\s*:/.test(trimmed) || /^run\s*\(/.test(trimmed)
83
+ if (isExemptStart) {
84
+ // Mark lines from here until the block closes (depth reaches 0 after opening)
85
+ let depth = 0
86
+ let started = false
87
+ const startLine = i
88
+ while (i < srcLines.length) {
89
+ exemptLines.add(i + 1) // 1-based
90
+ for (const ch of srcLines[i]) {
91
+ if (ch === '{' || ch === '(') { depth++; started = true }
92
+ if (ch === '}' || ch === ')') { depth-- }
93
+ }
94
+ if (started && depth <= 0) break
95
+ i++
96
+ }
97
+ }
98
+ i++
99
+ }
100
+
101
+ return lineNums.every(n => exemptLines.has(n))
102
+ }
103
+
104
+ const gaps = []
105
+ for (const { file, uncovered } of rawGaps) {
106
+ // Try to resolve relative to cwd (coverage reports use relative paths)
107
+ const candidates = [file, `src/pages/${file}`, file.replace(/^.*src\/pages\//, 'src/pages/')]
108
+ const resolved = candidates.find(p => fs.existsSync(p))
109
+
110
+ if (resolved && allLinesExempt(resolved, uncovered)) continue // auto-exempt
111
+
112
+ gaps.push(` ${file} — uncovered lines: ${uncovered}`)
113
+ }
114
+
115
+ if (gaps.length) {
116
+ process.stdout.write(JSON.stringify({
117
+ decision: 'block',
118
+ reason: [
119
+ 'COVERAGE GAPS detected. Add tests for these uncovered lines before finishing:',
120
+ ...gaps,
121
+ '',
122
+ 'Run `npm run test:coverage` to see the full report.',
123
+ 'Exempt from this rule: async action run() functions and server fetchers that call real APIs.',
124
+ 'Everything else — view branches, mutations, onViewError, pure helper functions — must be covered.',
125
+ ].join('\n'),
126
+ }))
127
+ }
@@ -0,0 +1,177 @@
1
+ ## Spec review checklist
2
+
3
+ Before finishing any spec, verify every point below. Fix anything that fails.
4
+
5
+ ### Critical
6
+
7
+ - **`meta` must be a plain object — never a function.** Individual fields (`title`, `description`, `styles`) can be `async (ctx) => value` functions, but `meta` itself is always `{}`.
8
+
9
+ ```js
10
+ // WRONG — meta is not a function factory
11
+ meta: async (ctx) => ({ title: '...', styles: [...] })
12
+
13
+ // CORRECT — meta is a plain object; individual fields are functions
14
+ meta: {
15
+ title: async (ctx) => (await post(ctx)).frontmatter.title,
16
+ description: async (ctx) => (await post(ctx)).frontmatter.description,
17
+ styles: ['/pulse-ui.css', '/app.css'],
18
+ }
19
+ ```
20
+
21
+ - **Do not set `hydrate` in specs.** It is auto-derived by the framework from the URL entry passed to `createServer`. Specs with `mutations`, `actions`, or `persist` are hydrated automatically. Purely server-rendered specs get zero JavaScript — no configuration needed.
22
+
23
+ - **Always restart the server and reload the browser after every file edit — before checking the result.** The dev server restarts on file changes but the browser tab stays stale. The required sequence is: `pulse_restart_server` → `navigate_page`. Never attempt to debug a visual problem without doing both steps first.
24
+
25
+ ### Components first — or creative override
26
+
27
+ - **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
28
+ - **Do not reinvent component patterns.** These patterns have dedicated components — never write custom HTML for them unless a creative override applies (see below):
29
+ - Hero sections → `hero({ title, subtitle, actions, image, ... })`
30
+ - Product/service cards → `card({ title, body, image, footer, ... })`
31
+ - Image + text two-column layouts → `media({ image, content, reverse })`
32
+ - Horizontal strips/banners → `banner({ content, variant })`
33
+ - Feature tiles → `feature({ icon, title, description })`
34
+ - Testimonials → `testimonial({ quote, name, role, src })`
35
+ - CTAs → `cta({ title, subtitle, actions })`
36
+ - Navigation → `nav({ logo, links, actions })`
37
+ - Footers → `footer({ logo, links, columns, legal, ... })`
38
+
39
+ - **Creative override — raw HTML throughout is permitted** when the design intent genuinely calls for it: a highly custom visual identity, an asymmetric or typographically-driven layout, a brutalist/editorial/retro/neon/paper vibe where components would constrain the expression. This is a deliberate design decision, not a shortcut. When taking this path:
40
+ - State it explicitly in the build brief: *"Building component-free — raw HTML throughout for creative control."*
41
+ - Functional atoms (`button`, `input`, `badge`, `modal`) should still come from components unless there is a specific design reason not to.
42
+ - `/pulse-ui.css` is always required — it provides the token system your custom CSS will use.
43
+ - The quality gate replaces the component checklist: **100/100/100/100 Lighthouse on both desktop and mobile is the pass bar.** A component-free page that passes every audit is correct. A component-heavy page that fails accessibility is not.
44
+
45
+ - When reviewing a spec built under creative override, do not flag raw section HTML as a violation — check the Lighthouse results instead.
46
+
47
+ - **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
48
+
49
+ ```js
50
+ import { appBadge } from '@invisibleloop/pulse/ui'
51
+
52
+ // In a hero() actions slot:
53
+ ${hero({
54
+ actions: `
55
+ ${appBadge({ store: 'apple', href: 'https://apps.apple.com/...' })}
56
+ ${appBadge({ store: 'google', href: 'https://play.google.com/...' })}
57
+ `,
58
+ })}
59
+ ```
60
+
61
+ ### Reuse (DRY)
62
+
63
+ - **Extract a view helper when the same HTML pattern appears 3 or more times in a single spec.** A plain JS function returning an HTML string is sufficient — no framework needed:
64
+ ```js
65
+ const card = ({ title, body }) => `<div class="card"><h3>${title}</h3><p>${body}</p></div>`
66
+ ```
67
+ - **Create a shared component in `src/ui/` when the same pattern is needed across 2 or more different specs.** Follow the existing pattern: a named export that returns an HTML string.
68
+ - **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Wait until the third use before extracting.
69
+
70
+ ### Correctness
71
+
72
+ - Mutations return plain partial-state objects and have no side effects (no fetch, no DOM access).
73
+ - `persist` contains only serialisable state that should survive a page reload — not ephemeral UI state like a loading flag or temporary selection.
74
+ - `e.target` assumptions in mutations are safe if the element has child nodes — use `e.target.closest('[data-index]')` rather than assuming `e.target` is the element with the attribute.
75
+ - State shape is consistent — avoid a single field that is sometimes `null`, sometimes a string, sometimes a boolean. Use a dedicated `status` field instead.
76
+ - **Never use `state.modalOpen` or conditional modal rendering.** This destroys the `<dialog>` on every render, breaking focus, animation, and native ESC handling. Instead, always render the `<dialog>` in the DOM and use `data-dialog-open="id"` to open it — the runtime handles this without any spec state:
77
+ ```html
78
+ <!-- always in the view, never conditional -->
79
+ ${modal({ id: 'confirm', title: 'Confirm', content: '...' })}
80
+ <!-- anywhere on the page — opens the dialog, no mutation needed -->
81
+ ${modalTrigger({ target: 'confirm', label: 'Open' })}
82
+ <!-- or inline: -->
83
+ <button data-dialog-open="confirm">Open</button>
84
+ ```
85
+ Close is handled natively by `<form method="dialog">` (inside the modal), ESC key, backdrop click, or `data-dialog-close` on any element.
86
+
87
+ ### Defensive data handling
88
+
89
+ - **Always check `res.ok` before parsing fetch responses.** Never call `res.json()` on a response that may have failed. Never use `await res.text()` as an error message — on a 404 or 500, this returns raw HTML which surfaces directly in toasts and error alerts. The correct pattern:
90
+ ```js
91
+ const res = await fetch('/api/...')
92
+ if (!res.ok) {
93
+ let message = `Request failed: ${res.status}`
94
+ try { const j = await res.json(); message = j.message || j.error || message } catch {}
95
+ throw new Error(message)
96
+ }
97
+ return await res.json()
98
+ ```
99
+ - **Never assume the shape of data from external APIs or server fetchers.** Use optional chaining (`?.`) and nullish coalescing (`??`) at every access point. If a server fetcher can return `null`, the view must handle it — `server.user?.name ?? 'Guest'` not `server.user.name`.
100
+ - **Validate FormData fields before use.** `formData.get('email')` returns `null` if the field is missing. Check for null/empty before passing to an API or database.
101
+ - **Do not trust URL params.** `ctx.params.id` is a raw string from the URL. Validate it before use — check it exists, is the right type, and refers to a real resource. Return a 404 or redirect if it doesn't.
102
+
103
+ ### CSS
104
+
105
+ - **`app.css` must contain no hex values or raw colour values.** A lint hook enforces this and will block the build. Hex values belong in `public/theme.css` as token definitions; `app.css` references them via `var()` only.
106
+ - **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
107
+
108
+ ### Security
109
+
110
+ - Any value from user input (URL params, form fields, external APIs) interpolated into view HTML must be escaped.
111
+
112
+ ### Tests
113
+
114
+ - Pure logic functions extracted from a spec (e.g. `checkResult`, `validate`, `formatPrice`) must have unit tests in a corresponding `.test.js` file.
115
+ - **When fixing a bug, write a failing test first.** The test must reproduce the bug before the fix is applied, then pass after. This pins the behaviour so the bug cannot silently return. A fix without a regression test is incomplete.
116
+ - **Use `renderSync` / `render` from `@invisibleloop/pulse/testing` to test view HTML output.** Do not test views with raw `html.includes()` — use the query helpers instead:
117
+ ```js
118
+ import { renderSync, render } from '@invisibleloop/pulse/testing'
119
+
120
+ // Sync — call view directly with mock state/server
121
+ const result = renderSync(mySpec, { state: { count: 5 }, server: { items: [] } })
122
+ assert(result.has('button'))
123
+ assert.equal(result.get('#count').text, '5')
124
+ assert.equal(result.count('li'), 0)
125
+
126
+ // Async — run real server fetchers (integration), or pass server to skip them
127
+ const result = await render(mySpec, { server: { product: mockProduct } })
128
+ assert.equal(result.get('h1').text, mockProduct.name)
129
+ assert.equal(result.attr('img', 'src'), mockProduct.image)
130
+ ```
131
+ Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, combinations (`button.primary[disabled]`), and **descendant combinators** (`result.count('.parent li')`, `result.find('tbody tr')`). Descendant selectors match the rightmost token within the ancestor context — they do not return 0 if the ancestor doesn't exist, they simply find no matching descendants.
132
+
133
+ ### Markdown
134
+
135
+ - **Use `md()` from `@invisibleloop/pulse/md` for `.md` file content.** Never read and parse markdown manually. Pass the result's `html` to `prose()`.
136
+ - **Call the same `md()` fetcher in both `meta` and `server`** — it caches on `ctx._mdCache` so the file is only read once per request. Do not create two separate fetchers for the same file.
137
+ - **Always add `onViewError` on dynamic markdown routes** (routes with `:param` segments loading `.md` files). A missing file throws `{ status: 404 }` which must be caught gracefully.
138
+
139
+ ### View error handling
140
+
141
+ - **Define `onViewError` on any page where the view could throw due to bad or missing data.** Without it, a runtime view error returns a 500 on the server and shows a generic inline message on the client. With it, the server returns 200 with your fallback HTML instead of a 500, and the client renders your fallback:
142
+ ```js
143
+ onViewError: (err, state, serverState) => `
144
+ <div class="u-p-4 u-text-center">
145
+ <p>Something went wrong. <a href="">Reload</a></p>
146
+ </div>
147
+ `
148
+ ```
149
+ Use this on pages that render data from external APIs or user-supplied content — any path where the view can encounter `null`, `undefined`, or unexpected shapes that would cause a crash. It is not required on simple pages with predictable data.
150
+
151
+ ### Store updates from actions
152
+
153
+ - **Use `_storeUpdate` to push changes to the global store from an action.** Return it from `onSuccess` alongside the local state update — it is stripped from page state and forwarded to the store. All mounted pages that subscribe to the affected keys re-render immediately:
154
+ ```js
155
+ onSuccess: (state, theme) => ({
156
+ saved: true,
157
+ _storeUpdate: { settings: { theme } }, // ← merged into store state
158
+ }),
159
+ ```
160
+ `_storeUpdate` only merges into the store — it does not appear in the page's own state. The rest of the return is merged into local state as normal. Use this instead of a full-page reload when a user action changes shared data (theme, cart count, user profile, etc.).
161
+
162
+ ### Accessibility
163
+
164
+ - Interactive elements without visible text have an `aria-label`.
165
+ - Disabled state is reflected with the `disabled` attribute, not just CSS.
166
+ - The page has a `<main id="main-content">` landmark.
167
+ - **Every element with `data-event`, `data-store-event`, `data-dialog-open`, or `data-dialog-close` is natively interactive (`button`, `a`, `input`, `select`, `textarea`, `summary`) or has `tabindex="0"`.** Non-interactive tags (`div`, `span`, `li`, `td`, etc.) with these attributes are not keyboard reachable. Prefer `<button>` — it is focusable, fires on Enter/Space, and carries the correct ARIA role for free. Only use `tabindex="0"` on a non-interactive element when a `<button>` genuinely cannot be used.
168
+ - **Every interactive element communicates its purpose and state:**
169
+ - Buttons have visible text or `aria-label`. Links use descriptive text — "click here", "here", "read more" are failures.
170
+ - Form inputs have an associated `<label for="id">` or `aria-label`. `placeholder` alone is not a label.
171
+ - Toggle controls (open/close, show/hide, expand/collapse) carry `aria-expanded="true|false"` or `aria-pressed="true|false"`.
172
+ - While an action is in flight, the trigger button has `aria-busy="true"` or its label changes (e.g. "Saving…"). A spinner alone is not enough.
173
+ - Active nav items have `aria-current="page"`. Selected items in a list, tab set, or option group have `aria-selected="true"`.
174
+ - **The page has a logical tab order:**
175
+ - No `tabindex` value greater than 0. Positive tabindex values override the natural DOM order and create a broken tab sequence. Remove them and reorder the DOM instead.
176
+ - Visually hidden interactive content (off-canvas menus, collapsed panels, `opacity:0` elements) is removed from the tab order with `tabindex="-1"` or the `inert` attribute — CSS alone does not remove an element from tab order.
177
+ - DOM order matches the visual reading order. When flexbox `order` or CSS grid placement visually reposition elements, the tab sequence still follows the DOM — author the DOM in the order a user would read and interact with the page.
@@ -0,0 +1 @@
1
+ 0.14.1