@elitedcs/ghl-mcp 3.83.1 → 3.83.3

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.
package/guide/guide.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>GHL Command — User Guide</title>
7
- <!-- guides-hash: c6cd1ce6e416eaff -->
7
+ <!-- guides-hash: 85b384b5142e9278 -->
8
8
  <style>
9
9
  /* Deliberately light in every environment, including a dark-mode OS. This is a
10
10
  reference document people read at length, print, and save to PDF, and a page
@@ -677,20 +677,21 @@ Then tell me which ones you can fix safely and which ones need a decision from m
677
677
  <div class="pro"><div class="pro-head"><b>The exact ask we run on real client accounts</b><span class="pill full">Proven live</span><button class="copy" type="button">Copy</button></div><p class="pro-body">Run the GHL Command Blueprint for a new client.
678
678
  The business: [Radiance Med Spa], a [med spa] in [Austin, Texas]. What they sell: [body contouring and injectables]. Their main goal for the next 90 days: [fill the consult calendar].
679
679
  Install the intake form in my [MCP Testing] account, give me the link to send them, and once their answers are in, build the plan from those answers.
680
- Show me the full plan before anything is created: pipeline and stages, custom fields, tags, calendar, form, funnel, the emails and texts written out, and every workflow. Mark clearly which items you will build and which ones I have to do myself in GHL.
680
+ Show me the full plan before anything is created: pipeline and stages, custom fields, tags, calendar, form, the emails and texts written out, and every workflow. Mark clearly which items you will build and which ones I have to do myself in GHL.
681
681
  Build nothing until I say approved.</p></div>
682
682
  <p class="gpara">Everything in [brackets] is yours to change. The last line is the gate. The Blueprint is designed to stop there and wait for a human.</p>
683
683
  </div>
684
684
  <div class="gsec"><h2>What happens when you ask</h2>
685
685
  <ol class="steps">
686
686
  <li>Claude installs the intake form in the account and gives you the link to send the client.</li>
687
- <li>When their answers land, it turns them into a build plan: pipeline, fields, tags, calendar, form, funnel, messages, workflows. Every message written out in full. The answers arrive as a <b>contact</b> in the account, tagged <code>blueprint-intake</code>, carrying every question as a field on that contact. That is where Claude reads them from.</li>
687
+ <li>When their answers land, it turns them into a build plan: pipeline, fields, tags, calendar, form, messages, workflows. Every message written out in full. The answers arrive as a <b>contact</b> in the account, tagged <code>blueprint-intake</code>, carrying every question as a field on that contact. That is where Claude reads them from.</li>
688
688
  <li>You read it and change anything in plain English. Then you approve, and only then does it start building. Before your approval the plan is a dry run: it resolves every reference and reports what it would do, and writes none of it.</li>
689
689
  <li>It builds in order, checks each piece exists after it creates it, and hands you the manual checklist for the parts that need a human in GHL.</li>
690
690
  </ol>
691
691
  </div>
692
692
  <div class="gsec"><h2>Good to know</h2>
693
693
  <ul>
694
+ <li><b>The Blueprint does not build a funnel, a page, or a website, and that is deliberate.</b> GoHighLevel's own page builder was never good enough to hand a client, so the build stops at the account: pipeline, fields, tags, calendar, form, messages and workflows. A plan that asks for a funnel is refused rather than half-built. <b>You have not lost funnels.</b> GHL Command still installs one of GoHighLevel's own designed funnels from its library (see *Build a funnel from a template*), still creates funnels and pages directly, and Page Studio still composes pages and whole sites. Pages are simply their own job, after the account exists, on hosting you choose.</li>
694
695
  <li>Triggers get scoped to the thing you actually meant. An appointment trigger can be tied to one calendar, so a caregiver interview does not start the family consultation sequence. A reply trigger can be narrowed to a tag or to whether the reply was a yes or a no, instead of firing on every inbound message. Missed calls can be caught by call outcome, and failed phone-number checks by validation state. Say which one you mean in plain English and the plan carries it through.</li>
695
696
  <li>If a workflow step moves a deal to a new stage, the plan makes it find the deal first. That pairing is checked before the build runs, because a stage move on its own saves cleanly and then does nothing at all.</li>
696
697
  <li>The manual list is short but real: connecting a payment provider, phone and A2P registration, connecting social accounts, and domain settings. Those need a human with account access, and no tool can honestly do them for you.</li>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.83.1",
3
+ "version": "3.83.3",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 250 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
@@ -15,11 +15,6 @@
15
15
  "src/command-os/design/rules/anti-default.json",
16
16
  "src/command-os/design/rules/web.json",
17
17
  "src/command-os/design/rules/anti-default-contract.mjs",
18
- "src/command-os/design/rules/check-static.mjs",
19
- "src/command-os/design/rules/fixtures/claude-default.html",
20
- "src/command-os/design/rules/fixtures/house-clean.html",
21
- "src/command-os/design/rules/fixtures/generator-run-1.html",
22
- "src/command-os/design/rules/fixtures/external-css.html",
23
18
  "dist/index.js",
24
19
  "dist/capture-helper.js",
25
20
  "templates/action-schemas.json",
@@ -37,6 +32,7 @@
37
32
  ],
38
33
  "scripts": {
39
34
  "build": "node src/command-os/assessment/build-app.mjs && esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external && esbuild src/capture-helper.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/capture-helper.js --packages=external",
35
+ "typecheck": "tsc --noEmit",
40
36
  "setup": "node setup-wizard.mjs",
41
37
  "catalogue": "node scripts/export-command-os-catalogue.mjs",
42
38
  "manifest:write": "node scripts/write-package-manifest.mjs",
@@ -1,238 +0,0 @@
1
- #!/usr/bin/env node
2
- // Proof harness for anti-default.json.
3
- //
4
- // This is NOT the verifier. The real verifier (design-studio-plan.md 3.6) renders the page
5
- // in the automation Chrome profile and reads computed styles. This runs only the subset of
6
- // rules that can be decided from the source text of a single self-contained HTML file, with
7
- // no browser and no dependencies. Its job is to prove the ruleset fires on a slop fixture
8
- // and stays silent on an honest one.
9
- //
10
- // Usage: node check-static.mjs [file.html ...] (no args = all four fixtures)
11
-
12
- import { readFileSync } from 'node:fs';
13
- import { fileURLToPath } from 'node:url';
14
- import { dirname, join, basename, resolve } from 'node:path';
15
- import { assertAntiDefaultContract } from './anti-default-contract.mjs';
16
-
17
- const here = dirname(fileURLToPath(import.meta.url));
18
- const RULES = JSON.parse(readFileSync(join(here, 'anti-default.json'), 'utf8'));
19
- assertAntiDefaultContract(RULES, JSON.parse(readFileSync(join(here, 'web.json'), 'utf8')));
20
- const byId = Object.fromEntries(RULES.rules.map(r => [r.id, r]));
21
-
22
- const srgb = c => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
23
- const lum = hex => {
24
- const h = hex.replace('#', '');
25
- const f = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
26
- const [r, g, b] = [0, 2, 4].map(i => parseInt(f.slice(i, i + 2), 16));
27
- return 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b);
28
- };
29
- const contrast = (a, b) => { const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };
30
-
31
- function parse(html) {
32
- const css = [...html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)].map(m => m[1]).join('\n');
33
- const body = (html.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, html])[1];
34
- const strip = s => s.replace(/<(script|style)[\s\S]*?<\/\1>/gi, ' ').replace(/<[^>]+>/g, ' ');
35
- const text = strip(body).replace(/&nbsp;/g, ' ').replace(/[ \t]+/g, ' ');
36
- const attrs = [...html.matchAll(/\b(alt|aria-label|title|placeholder)\s*=\s*"([^"]*)"/gi)].map(m => m[2]).join(' \n ');
37
- const docTitle = (html.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [, ''])[1];
38
- const heroBlock = (body.match(/<section[^>]*class="[^"]*hero[^"]*"[\s\S]*?<\/section>/i) || [''])[0];
39
- const heroText = strip(heroBlock);
40
- const headings = [...body.matchAll(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi)].map(m => strip(m[1]));
41
- const rule = sel => {
42
- const re = new RegExp(sel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*\\{([^}]*)\\}', 'i');
43
- return (css.match(re) || [, ''])[1];
44
- };
45
- const vars = Object.fromEntries([...css.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map(m => [m[1], m[2].trim()]));
46
- const resolve = v => (v || '').replace(/var\((--[\w-]+)\)/g, (_, n) => vars[n] || '');
47
- const cssPresent = css.trim().length > 0;
48
- const externalCss = /<link[^>]+rel\s*=\s*["']?stylesheet/i.test(html);
49
- return { css, body, text, attrs, heroText, headings, rule, resolve, cssPresent, externalCss,
50
- all: text + '\n' + attrs + '\n' + docTitle };
51
- }
52
-
53
- // A check returns one of three things, never two:
54
- // null the check ran and the page is clean
55
- // U the check could not decide from source (the subject was not visible here)
56
- // text the check ran and the page trips the rule, with what was measured
57
- // The third state is the whole point. "The source does not show it" is NOT
58
- // "the page does not do it": styles can live in an external sheet, a utility
59
- // framework, or JS. A harness without U reports an unreadable page as clean.
60
- const U = '\u0000UNDECIDABLE';
61
-
62
- // Checks that cannot be decided at all when no inline CSS is present.
63
- const CSS_DEPENDENT = new Set(['AD01','AD02','AD03','AD10','AD11','AD12','AD15','AD16','AD20',
64
- 'AD27','AD28','AD29','AD42','AD43','AD44','AD59','AD60','AD61','AD62','AD63','AD64','AD65','AD66','AD69']);
65
-
66
- // id -> (doc) => null | U | string
67
- const CHECKS = {
68
- AD01: d => { const f = (d.rule('body').match(/font-family\s*:\s*([^;]+)/i) || [, ''])[1]
69
- + ',' + (d.rule('h1, h2, h3').match(/font-family\s*:\s*([^;]+)/i) || [, ''])[1];
70
- const hit = ['Inter', 'Space Grotesk', 'Roboto', 'Arial', 'Helvetica'].filter(n => new RegExp(`(^|[,'"\\s])${n}\\b`, 'i').test(f));
71
- return hit.length ? `display/body face = ${hit.join(', ')}` : null; },
72
- AD02: d => { const m = d.rule('body').match(/font-size\s*:\s*(\d+(?:\.\d+)?)px/i);
73
- if (!m) return U;
74
- return +m[1] < 18 ? `body font-size = ${m[1]}px (floor 18px)` : null; },
75
- AD03: d => { const b = d.rule('body'); const hex = v => (d.resolve(v || '').match(/#[0-9a-f]{3,6}\b/i) || [])[0];
76
- const fg = hex((b.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i) || [])[1]);
77
- const bg = hex((b.match(/background(?:-color)?\s*:\s*([^;]+)/i) || [])[1]);
78
- if (!fg || !bg) return U;
79
- const r = contrast(fg, bg); return r < 7 ? `body ${fg} on ${bg} = ${r.toFixed(2)}:1 (floor 7.0:1)` : null; },
80
- AD10: d => /text-transform\s*:\s*uppercase/i.test(d.rule('body')) ? 'body text-transform: uppercase' : null,
81
- AD11: d => /Fraunces|Instrument[_ ]Serif/i.test(d.css) ? 'reflex serif in the stack' : null,
82
- AD12: d => { const b = d.rule('body'); if (!b.trim()) return U;
83
- const val = re => d.resolve((b.match(re) || [])[1] || '').trim();
84
- const fgRaw = val(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
85
- const bgRaw = val(/background(?:-color)?\s*:\s*([^;]+)/i);
86
- if (!fgRaw || !bgRaw) return U;
87
- const isBlack = v => /^(#0{3}|#0{6}|black)$/i.test(v), isWhite = v => /^(#f{3}|#f{6}|white)$/i.test(v);
88
- const hRaw = d.resolve((d.rule('h1, h2, h3').match(/color\s*:\s*([^;]+)/i) || [])[1] || '').trim();
89
- if (isBlack(fgRaw) && isWhite(bgRaw)) return `pure ${fgRaw} on ${bgRaw}`;
90
- if (isWhite(fgRaw) && isBlack(bgRaw)) return `pure ${fgRaw} on ${bgRaw}`;
91
- if (isBlack(hRaw) && isWhite(bgRaw)) return `pure ${hRaw} heading on ${bgRaw}`;
92
- return null; },
93
- AD15: d => /(?:\.hero|header)[^{]*\{[^}]*background[^;}]*(linear|radial)-gradient/i.test(d.css) ? 'gradient on the hero ground' : null,
94
- AD16: d => /background-clip\s*:\s*text/i.test(d.css) && /gradient/i.test(d.css) ? 'background-clip: text over a gradient' : null,
95
- AD20: d => {
96
- let decided = false;
97
- const sels = [...d.css.matchAll(/([^{}]+)\{([^}]*)\}/g)]
98
- .filter(m => /(\bbutton|\.cta\b|\.btn\b|\[type=["']?submit)/i.test(m[1]));
99
- for (const [, sel, block] of sels) {
100
- const fg = d.resolve((block.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i) || [, ''])[1]).trim();
101
- const bg = d.resolve((block.match(/background(?:-color)?\s*:\s*([^;]+)/i) || [, ''])[1]).trim();
102
- const hex = v => (v.match(/#[0-9a-f]{3,8}/i) || [])[0];
103
- if (!hex(fg) || !hex(bg)) continue;
104
- const r = contrast(hex(fg), hex(bg));
105
- if (r < 4.5) return `CTA label ${hex(fg)} on ${hex(bg)} = ${r.toFixed(2)}:1 (floor 4.5:1) on "${sel.trim()}"`;
106
- decided = true;
107
- }
108
- if (!decided && /<button|type=["']?submit|class="[^"]*\b(cta|btn)\b/i.test(d.body)) return U;
109
- return null; },
110
- AD27: d => { const m = [...d.css.matchAll(/border-radius\s*:\s*(\d+)px/gi)].map(x => +x[1]).filter(v => v >= 24);
111
- return m.length ? `border-radius ${m.join('px, ')}px (cards cap at 16px)` : null; },
112
- AD28: d => { const hit = [...d.css.matchAll(/\{([^}]*)\}/g)].filter(b =>
113
- /border\s*:\s*1px/i.test(b[1]) && /box-shadow\s*:[^;]*?\b(\d+)px\s+(\d+)px/i.test(b[1]) &&
114
- +(b[1].match(/box-shadow\s*:[^;]*?\b\d+px\s+(\d+)px/i) || [, 0])[1] >= 16);
115
- return hit.length ? `1px border + ${(hit[0][1].match(/box-shadow\s*:[^;]*?\b\d+px\s+(\d+)px/i))[1]}px shadow blur on one element` : null; },
116
- AD29: d => { const z = [...d.css.matchAll(/z-index\s*:\s*(\d+)/gi)].map(x => +x[1]).filter(v => v >= 999);
117
- return z.length ? `z-index ${z.join(', ')}` : null; },
118
- AD42: d => { const m = d.css.match(/border-(?:left|right)\s*:\s*(\d+)px/i);
119
- return m && +m[1] > 1 ? `${m[1]}px side-stripe border` : null; },
120
- AD43: d => /backdrop-filter\s*:[^;]*blur/i.test(d.css) ? 'backdrop-filter: blur' : null,
121
- AD44: d => /repeating-(linear|radial)-gradient/i.test(d.css) ? 'repeating-gradient background' : null,
122
- AD45: d => /feTurbulence|feDisplacementMap|\b(sketch|doodle|squiggle|hand-drawn)\b/i.test(d.body + d.css) ? 'sketchy-SVG marker' : null,
123
- AD46: d => { const e = d.headings.filter(h => /\p{Extended_Pictographic}/u.test(h));
124
- return e.length ? `emoji in heading: ${e[0].trim().slice(0, 40)}` : null; },
125
- AD48: d => { const bad = d.text.split('\n').map(l => (l.match(/·/g) || []).length).filter(n => n > 1);
126
- return bad.length ? `${Math.max(...bad)} middle dots on one line` : null; },
127
- AD50: d => !d.heroText.trim() ? U : /(^|\s)(↓\s*)?scroll( to (explore|discover|begin|walk))?\b/i.test(d.heroText) ? 'scroll cue in the hero' : null,
128
- AD53: d => { if (!d.heroText.trim()) return U; const m = d.heroText.match(/\bv\d+\.\d+(\.\d+)?\b|\b(beta|alpha|early access|invite[- ]only|preview|waitlist)\b/i);
129
- return m ? `hero status label "${m[0]}"` : null; },
130
- AD59: d => /cursor\s*:\s*url\(/i.test(d.css) ? 'custom cursor image' : null,
131
- AD60: d => /transition\s*:\s*all\b/i.test(d.css) ? 'transition: all' : null,
132
- AD61: d => { const m = [...d.css.matchAll(/scale\((0(?:\.\d+)?)\)/gi)].map(x => +x[1]).filter(v => v <= 0.5);
133
- return m.length ? `entry from scale(${m[0]})` : null; },
134
- AD62: d => { const m = d.css.match(/(?:transition|animation)[^;{}]*\bease-in\b(?!-out)/i);
135
- return m ? 'ease-in timing on an interactive element' : null; },
136
- AD63: d => { const m = [...d.css.matchAll(/(?:transition|animation)[^;{}]*?(\d{3,5})ms/gi)].map(x => +x[1]).filter(v => v > 300);
137
- return m.length ? `${Math.max(...m)}ms UI duration (budget 300ms)` : null; },
138
- AD64: d => { const m = d.css.match(/transition\s*:[^;{}]*\b(width|height|top|left|right|bottom|margin|padding)\b/i);
139
- return m ? `animating layout property "${m[1]}"` : null; },
140
- AD65: d => (/transition|@keyframes|animation\s*:/i.test(d.css) && !/prefers-reduced-motion/i.test(d.css))
141
- ? 'motion present, no prefers-reduced-motion block' : null,
142
- AD66: d => { const hov = [...d.css.matchAll(/:hover\s*\{([^}]*)\}/gi)].filter(m => /transform\s*:/i.test(m[1]));
143
- if (!hov.length) return null;
144
- const gated = /@media\s*\(hover:\s*hover\)[^{]*\{[\s\S]*?:hover\s*\{[^}]*transform/i.test(d.css);
145
- return gated ? null : 'hover transform not gated by @media (hover: hover)'; },
146
- AD69: d => { const off = /outline\s*:\s*(none|0)\b/i.test(d.css);
147
- return off && !/:focus-visible/i.test(d.css) ? 'outline removed with no :focus-visible replacement' : null; },
148
- AD70: d => { const inputs = [...d.body.matchAll(/<(input|textarea)\b[^>]*>/gi)].map(m => m[0])
149
- .filter(t => /placeholder\s*=/.test(t) && !/aria-label/.test(t))
150
- .filter(t => { const id = (t.match(/\bid\s*=\s*"([^"]+)"/i) || [])[1];
151
- return !id || !new RegExp(`<label[^>]*for\\s*=\\s*"${id}"`, 'i').test(d.body); });
152
- return inputs.length ? `${inputs.length} placeholder-as-label field(s)` : null; },
153
- AD74: d => { const n = (d.all.match(/[—–]/g) || []).length;
154
- return n ? `${n} em/en dash character(s) in rendered text or attributes` : null; },
155
- AD75: d => { const m = d.all.match(/\b(john|jane|joe|jack)\s+(doe|smith|su|chan)\b|\blorem ipsum\b/i); return m ? `"${m[0]}"` : null; },
156
- AD76: d => { const m = d.all.match(/\b(acme|nexus|smartflow|cloudly|innovatech|globex|initech)\b/i); return m ? `"${m[0]}"` : null; },
157
- AD77: d => { const m = d.all.match(/\b(99\.9{1,2}%|100%|10x|50%|1,?234,?567)\b/i); return m ? `"${m[0]}"` : null; },
158
- AD78: d => { const m = d.all.match(/\b(elevate|seamless(ly)?|unleash|supercharge|streamline|empower(s|ing)?|world[- ]class|next[- ]gen(eration)?|revolutioni[sz]e|cutting[- ]edge|game[- ]chang\w*)\b/i); return m ? `"${m[0]}"` : null; },
159
- AD79: d => { const m = d.all.match(/\b(quietly (in use|trusted)|from the field|field notes|on our desks|currently on the bench|loose plates)\b/i); return m ? `"${m[0]}"` : null; },
160
- AD80: d => { const m = d.all.match(/\b(stage|step|phase|pass)\s*(one|two|three|four|0?\d)\b/i); return m ? `"${m[0]}"` : null; },
161
- AD81: d => { const m = d.all.match(/\b\w+ theat(er|re)\b|\bnot just (a |an )?\w+,? it'?s\b|\bactually \w+, not\b/i); return m ? `"${m[0]}"` : null; },
162
- };
163
-
164
- export const implemented = Object.keys(CHECKS);
165
-
166
- // Keep source coverage separate from canonical findings: an undecided check is
167
- // information even if another check of the same canonical rule finds a failure.
168
- export function gradeStatic(html) {
169
- const doc = parse(html);
170
- const results = implemented.map(id => {
171
- const value = CSS_DEPENDENT.has(id) && !doc.cssPresent ? U : CHECKS[id](doc);
172
- return { sourceRuleId: id, ruleId: byId[id].duplicateOf || id,
173
- check: byId[id].check, method: 'static-harness',
174
- state: value === U ? 'UNDECIDABLE' : value === null ? 'CLEAN' : 'FINDING',
175
- detail: value === U ? 'Could not decide from source; rendered evidence is required.' : value };
176
- });
177
- const group = rows => {
178
- const merged = new Map();
179
- for (const row of rows) {
180
- const prior = merged.get(row.ruleId);
181
- merged.set(row.ruleId, { ruleId: row.ruleId, check: row.check, method: row.method,
182
- state: row.state, sourceRuleIds: [...(prior?.sourceRuleIds || []), row.sourceRuleId],
183
- detail: prior ? `${prior.detail}; ${row.detail}` : row.detail });
184
- }
185
- return [...merged.values()];
186
- };
187
- const findings = group(results.filter(row => row.state === 'FINDING'));
188
- const undecided = group(results.filter(row => row.state === 'UNDECIDABLE'));
189
- const undecidableCount = results.filter(row => row.state === 'UNDECIDABLE').length;
190
- const judged = RULES.rules.filter(rule => rule.check === 'judged').map(rule => ({
191
- ruleId: rule.id, check: 'judged', state: 'ASSERTED',
192
- detail: `${rule.name}. Check: ${rule.howMeasured} Guidance: ${rule.rewrite} This rule requires model judgment and is never scored.`,
193
- }));
194
- return { findings, undecided, judged, results, decidedCount: results.length - undecidableCount,
195
- undecidableCount, litCount: results.filter(row => row.state === 'FINDING').length,
196
- externalCssUnread: doc.externalCss && !doc.cssPresent };
197
- }
198
-
199
- export function main(args = process.argv.slice(2)) {
200
- const targets = args.length ? args
201
- : ['claude-default.html', 'house-clean.html', 'generator-run-1.html', 'external-css.html']
202
- .map(f => join(here, 'fixtures', f));
203
- console.log(`anti-default.json ${RULES.rules.length} rules (${RULES.rules.filter(r => r.check === 'measured').length} measured, ` +
204
- `${RULES.rules.filter(r => r.check === 'proxy').length} proxy, ${RULES.rules.filter(r => r.check === 'judged').length} judged)`);
205
- console.log(`static harness ${implemented.length} source-decidable checks; per-page coverage is reported below. The rest need the rendered DOM.\n`);
206
-
207
- let exit = 0;
208
- for (const file of targets) {
209
- const report = gradeStatic(readFileSync(file, 'utf8'));
210
- const { findings, undecided, judged, litCount, decidedCount, undecidableCount } = report;
211
- const name = basename(file);
212
- const lit = new Set(findings.flatMap(row => row.sourceRuleIds));
213
- // These are fixture expectations, not a claim that the page passed design review.
214
- const EXPECT = {
215
- 'claude-default.html': { bar: 'must trip >= 5', ok: () => litCount >= 5 },
216
- 'house-clean.html': { bar: 'must trip 0 with nothing undecided',
217
- ok: () => litCount === 0 && undecidableCount === 0 },
218
- 'generator-run-1.html': { bar: 'must trip AD02 + AD20, and nothing else',
219
- ok: () => lit.has('AD02') && lit.has('AD20') && litCount === 2 },
220
- 'external-css.html': { bar: 'must decide nothing it cannot see',
221
- ok: () => litCount === 0 && undecidableCount === 26 &&
222
- [...CSS_DEPENDENT, 'AD50', 'AD53'].every(id => report.results.some(row => row.sourceRuleId === id && row.state === 'UNDECIDABLE')) },
223
- };
224
- const spec = EXPECT[name];
225
- const ok = spec?.ok();
226
- if (spec && !ok) exit = 1;
227
- console.log(`${spec ? (ok ? 'PASS' : 'FAIL') : 'REPORT'} ${name} ${litCount} lit, ${decidedCount} decided, ` +
228
- `${undecidableCount} undecidable (${spec?.bar || 'no fixture bar set'})`);
229
- for (const finding of findings) console.log(` ${finding.ruleId} ${finding.detail}`);
230
- for (const row of undecided) console.log(` ${row.ruleId} UNDECIDABLE ${row.detail} (source: ${row.sourceRuleIds.join(' ')})`);
231
- if (report.externalCssUnread) console.log(' Styles are in an external sheet this harness never reads.');
232
- for (const rule of judged) console.log(` ${rule.ruleId} JUDGED ${rule.detail}`);
233
- console.log('');
234
- }
235
- return exit;
236
- }
237
-
238
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = main();
@@ -1,55 +0,0 @@
1
- <!doctype html>
2
- <html lang="en"><head><meta charset="utf-8"><title>Nexus — Elevate Your Workflow</title>
3
- <style>
4
- :root { --brand: #7c3aed; }
5
- body { font-family: Inter, sans-serif; color: #666666; background: #ffffff; font-size: 16px; margin: 0; }
6
- h1, h2, h3 { font-family: 'Space Grotesk', sans-serif; color: #000000; }
7
- .hero { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding-top: 180px; padding-bottom: 120px; text-align: center; }
8
- .hero h1 { font-size: 112px; letter-spacing: -0.06em; background: linear-gradient(90deg,#fff,#c4b5fd); -webkit-background-clip: text; background-clip: text; color: transparent; }
9
- .eyebrow { text-transform: uppercase; letter-spacing: 0.18em; font-size: 12px; }
10
- .card { border: 1px solid #e5e7eb; box-shadow: 0 8px 32px rgba(124,58,237,0.25); border-radius: 32px; padding: 24px; background: #fff; }
11
- .card::before { content: ""; }
12
- .rail { border-left: 4px solid var(--brand); padding-left: 16px; }
13
- .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); display: inline-block; }
14
- .stripes { background: repeating-linear-gradient(45deg, #f3f4f6 0 8px, #fff 8px 16px); }
15
- .sticky { position: fixed; z-index: 9999; }
16
- button { transition: all 420ms ease-in; outline: none; cursor: url(/cursor.png), auto; }
17
- button:hover { transform: scale(1.05); }
18
- .fade { animation: pop 600ms ease-in forwards; }
19
- @keyframes pop { from { transform: scale(0); opacity: 0 } to { transform: scale(1); opacity: 1 } }
20
- .grow { transition: height 500ms linear, padding 500ms linear; }
21
- </style></head>
22
- <body>
23
- <nav><span class="dot"></span> Product · Pricing · Docs · Company · Blog</nav>
24
- <section class="hero">
25
- <p class="eyebrow">v0.6 · INVITE-ONLY PREVIEW</p>
26
- <h1>Elevate Your Workflow</h1>
27
- <p>The seamless, world-class platform that empowers teams to unleash their potential — built for the next generation of work.</p>
28
- <button>Get Started</button> <button>Start Now</button>
29
- <p class="eyebrow">BUILD. SHIP. SCALE.</p>
30
- <p>Lisbon 14:23 · 18°C</p>
31
- <p>↓ Scroll to explore</p>
32
- </section>
33
- <section class="stripes">
34
- <p class="eyebrow">01 · Features</p>
35
- <h2>Why teams choose Nexus 🚀</h2>
36
- <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:24px">
37
- <div class="card"><h3>Fast</h3><p>Lorem ipsum dolor sit amet, built to scale.</p></div>
38
- <div class="card"><h3>Secure</h3><p>Lorem ipsum dolor sit amet, built to scale.</p></div>
39
- <div class="card"><h3>Simple</h3><p>Lorem ipsum dolor sit amet, built to scale.</p></div>
40
- </div>
41
- </section>
42
- <section>
43
- <p class="eyebrow">02 · Proof</p>
44
- <h2>Quietly in use at</h2>
45
- <p>99.99% uptime · 10x faster · trusted by 50% of the Fortune 500</p>
46
- <blockquote>Productivity theater is over. — Jane Doe, VP of Operations</blockquote>
47
- </section>
48
- <section class="rail">
49
- <p class="eyebrow">03 · Process</p>
50
- <h2>How it works</h2>
51
- <p>Stage 1: Install. Stage 2: Configure. Stage 3: Ship.</p>
52
- <form><input type="email" placeholder="Enter your work email"></form>
53
- </section>
54
- <footer><p>v1.4.2 · Build 0048 · last sync 4s ago · main</p></footer>
55
- </body></html>
@@ -1,7 +0,0 @@
1
- <!doctype html><html><head><meta charset="utf-8"><title>Adversary</title>
2
- <link rel="stylesheet" href="/styles.css"></head>
3
- <body class="text-[13px] text-gray-400">
4
- <main><h1 class="text-6xl">Headline</h1>
5
- <p>Body copy that is 13px and low contrast, but you cannot see that from here.</p>
6
- <button class="bg-gray-100 text-white">Book an assessment</button></main>
7
- </body></html>
@@ -1,31 +0,0 @@
1
- <!doctype html>
2
- <html lang="en"><head><meta charset="utf-8"><title>Generator run 1, reproduction</title>
3
- <!--
4
- Reproduction of the two failures design-run's standalone generator shipped on Jerry's own
5
- brand (3 passes, 26 minutes, reported by the ghl-command-mcp lane 2026-09-08):
6
- 13px body text, and a call-to-action at roughly 1.15:1.
7
- This fixture exists to answer one question: would anti-default.json have caught them?
8
- It is deliberately clean in every other respect, so anything else that lights up is a false positive.
9
- -->
10
- <style>
11
- :root { --ink: #232833; --body: #4a5163; --ground: #ffffff; --line: #e3e3df; --ease-out: cubic-bezier(0.23, 1, 0.32, 1); }
12
- body { font-family: "Sora", system-ui, sans-serif; color: var(--body); background: var(--ground);
13
- font-size: 13px; line-height: 1.55; margin: 0; }
14
- h1 { font-family: "Sora", system-ui, sans-serif; color: var(--ink); font-size: 46px; line-height: 1.15; letter-spacing: -0.02em; }
15
- main { max-width: 66ch; margin: 0 auto; padding: 48px 24px; }
16
- .cta { background: #efefef; color: #ffffff; border: 0; border-radius: 6px;
17
- min-height: 48px; min-width: 48px; padding: 14px 28px; font-size: 17px;
18
- transition: transform 160ms var(--ease-out); }
19
- .cta:active { transform: scale(0.97); }
20
- .cta:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
21
- @media (hover: hover) and (pointer: fine) { .cta:hover { transform: translateY(-1px); } }
22
- label { display: block; color: var(--ink); font-size: 16px; margin-bottom: 6px; }
23
- input { font-size: 17px; padding: 12px; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
24
- @media (prefers-reduced-motion: reduce) { .cta { transition: none; } .cta:active { transform: none; } }
25
- </style></head>
26
- <body><main>
27
- <h1>A shorter recovery, planned before you pay</h1>
28
- <p>We map what is happening, then treat it in a fixed number of visits you agree to up front. No open-ended plans.</p>
29
- <button class="cta">Book an assessment</button>
30
- <form><label for="e">Your email</label><input id="e" type="email"></form>
31
- </main></body></html>
@@ -1,59 +0,0 @@
1
- <!doctype html>
2
- <html lang="en"><head><meta charset="utf-8"><title>Copper Ridge Chiropractic</title>
3
- <style>
4
- :root {
5
- --ink: #262b36; --body: #4a5163; --ground: #f7f7f5; --surface: #ffffff;
6
- --accent: #1f5f4f; --line: #dcdcd6;
7
- --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
8
- }
9
- body { font-family: "Source Serif 4", Georgia, serif; color: var(--body); background: var(--ground);
10
- font-size: 19px; line-height: 1.55; margin: 0; }
11
- h1, h2 { font-family: "Sora", system-ui, sans-serif; color: var(--ink); line-height: 1.15; letter-spacing: -0.02em; }
12
- h1 { font-size: 58px; }
13
- h2 { font-size: 34px; }
14
- main { max-width: 68ch; margin: 0 auto; padding: 0 24px; }
15
- .hero { background: var(--surface); padding-top: 72px; padding-bottom: 64px; border-bottom: 1px solid var(--line); }
16
- .cta { background: var(--accent); color: var(--surface); border: 0; border-radius: 6px;
17
- min-height: 48px; min-width: 48px; padding: 14px 28px; font-size: 18px;
18
- transition: transform 160ms var(--ease-out), background-color 160ms var(--ease-out); }
19
- .cta:active { transform: scale(0.97); }
20
- .cta:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
21
- @media (hover: hover) and (pointer: fine) { .cta:hover { background: #17493c; } }
22
- label { display: block; color: var(--ink); font-size: 17px; margin-bottom: 6px; }
23
- input { font-size: 18px; padding: 12px; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
24
- input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
25
- .panel { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 28px; }
26
- .split { display: grid; grid-template-columns: 1.4fr 1fr; gap: 40px; align-items: start; }
27
- @media (prefers-reduced-motion: reduce) {
28
- .cta { transition: background-color 160ms linear; }
29
- .cta:active { transform: none; }
30
- }
31
- </style></head>
32
- <body>
33
- <nav><a href="/care">Care</a> <a href="/team">Our team</a> <a href="/visit">Plan a visit</a></nav>
34
- <section class="hero"><main>
35
- <h1>Back pain that has a plan behind it</h1>
36
- <p>We map what is actually happening in your spine, then treat it in a fixed number of visits you agree to up front.</p>
37
- <button class="cta">Book an assessment</button>
38
- </main></section>
39
- <main>
40
- <section class="split">
41
- <div>
42
- <h2>What the first visit covers</h2>
43
- <p>Ninety minutes. A movement screen, imaging review if you have it, and a written plan with the visit count on it before you pay for anything.</p>
44
- </div>
45
- <div class="panel">
46
- <p>Dr. Amara Okonkwo has treated 1,340 patients in Copper Ridge since 2019.</p>
47
- </div>
48
- </section>
49
- <section>
50
- <h2>Book a first visit</h2>
51
- <form>
52
- <label for="email">Your email</label>
53
- <input id="email" type="email" name="email">
54
- <button class="cta" type="submit">Request a time</button>
55
- </form>
56
- </section>
57
- </main>
58
- <footer><main><p>Copper Ridge Chiropractic, 118 Wexler Ave. (406) 555-0173</p></main></footer>
59
- </body></html>