@elitedcs/ghl-mcp 3.82.0 → 3.83.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.
@@ -0,0 +1,238 @@
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();
@@ -0,0 +1,55 @@
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>
@@ -0,0 +1,7 @@
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>
@@ -0,0 +1,31 @@
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>
@@ -0,0 +1,59 @@
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>
@@ -0,0 +1,222 @@
1
+ [
2
+ {
3
+ "id": "W1",
4
+ "medium": "web",
5
+ "name": "Mobile-first, capped measure",
6
+ "threshold": "no horizontal overflow at 320; shell max-width 1200–1440; prose ≤75ch; min-width media queries",
7
+ "unit": "px / ch",
8
+ "source": "Marcotte; Frost",
9
+ "check": "measured",
10
+ "howMeasured": "Document scroll width at 320 px; the widest centred container's max-width; prose max-width; media query direction."
11
+ },
12
+ {
13
+ "id": "W2",
14
+ "medium": "web",
15
+ "name": "Content-driven breakpoints",
16
+ "threshold": "breakpoints near 640/768/1024/1280; zero horizontal scroll at 320, 390, 768, 1024, 1440",
17
+ "unit": "px",
18
+ "source": "Frost; Tailwind",
19
+ "check": "measured",
20
+ "howMeasured": "Media query widths read from the stylesheet; document scroll width at five viewports."
21
+ },
22
+ {
23
+ "id": "W3",
24
+ "medium": "web",
25
+ "name": "Fluid type via clamp()",
26
+ "threshold": "h1 and body font-size use clamp(); no bare vw on type",
27
+ "unit": "",
28
+ "source": "Utopia, Mudford & Gilyead",
29
+ "check": "measured",
30
+ "howMeasured": "Stylesheet font-size declarations on h1 and body selectors inspected for clamp() and bare vw."
31
+ },
32
+ {
33
+ "id": "W4",
34
+ "medium": "web",
35
+ "name": "Body size and tap targets",
36
+ "threshold": "body ≥16 px; buttons, form controls, CTAs and nav links ≥44×44 px; any other link ≥24×24 px",
37
+ "unit": "px",
38
+ "source": "Apple HIG (44); WCAG 2.5.8 (24)",
39
+ "check": "measured",
40
+ "howMeasured": "Computed body font-size; bounding boxes of links, buttons and inputs at 390 px, tiered by role."
41
+ },
42
+ {
43
+ "id": "W5",
44
+ "medium": "web",
45
+ "name": "One primary action per view",
46
+ "threshold": "≤1 primary control per viewport-height slice",
47
+ "unit": "controls",
48
+ "source": "Krug; Hick's Law",
49
+ "check": "proxy",
50
+ "howMeasured": "Primary controls derived by accent fill (background within a colour distance of the brand accent); the file's data-role only cross-checked."
51
+ },
52
+ {
53
+ "id": "W6",
54
+ "medium": "web",
55
+ "name": "The fold answers what, who, next",
56
+ "threshold": "h1 ≤12 words, a subline and the primary control inside the first viewport at 1366×768 and 390×844; nav ≤72 px on mobile",
57
+ "unit": "",
58
+ "source": "NN/g; Krug",
59
+ "check": "proxy",
60
+ "howMeasured": "Positions of h1, first paragraph and first primary control against the viewport; the comprehension claim is ASSERTED."
61
+ },
62
+ {
63
+ "id": "W7",
64
+ "medium": "web",
65
+ "name": "Scan patterns; left-aligned body",
66
+ "threshold": "body paragraphs never justified; prose not centred",
67
+ "unit": "",
68
+ "source": "NN/g; Butterick",
69
+ "check": "measured",
70
+ "howMeasured": "Computed text-align on paragraphs longer than 40 words."
71
+ },
72
+ {
73
+ "id": "W8",
74
+ "medium": "web",
75
+ "name": "Measure 45–75 characters",
76
+ "threshold": "45–75 characters per rendered line on prose",
77
+ "unit": "characters",
78
+ "source": "Bringhurst; Butterick",
79
+ "check": "measured",
80
+ "howMeasured": "Per rendered line via Range rectangles, as S10, on paragraphs of 40+ words at 1440 px."
81
+ },
82
+ {
83
+ "id": "W9",
84
+ "medium": "web",
85
+ "name": "Line height and paragraph spacing",
86
+ "threshold": "body ≥1.5, display 1.0–1.25, paragraph spacing ≥0.75em",
87
+ "unit": "ratio / em",
88
+ "source": "WCAG 1.4.12; Bringhurst",
89
+ "check": "measured",
90
+ "howMeasured": "Computed line-height ÷ font-size and margin-bottom ÷ font-size on paragraphs and headings."
91
+ },
92
+ {
93
+ "id": "W10",
94
+ "medium": "web",
95
+ "name": "8-point spacing, one type scale",
96
+ "threshold": "spacing in {4,8,12,16,24,32,48,64,96,128}; one modular scale",
97
+ "unit": "px",
98
+ "source": "Material; Bryn Jackson",
99
+ "check": "measured",
100
+ "howMeasured": "Computed margin, padding and gap values; distinct font sizes checked for a consistent ratio."
101
+ },
102
+ {
103
+ "id": "W11",
104
+ "medium": "web",
105
+ "name": "WCAG 2.2 AA contrast",
106
+ "threshold": "text ≥4.5:1, large text ≥3:1, UI ≥3:1",
107
+ "unit": "ratio",
108
+ "source": "WCAG 2.2 1.4.3 / 1.4.11",
109
+ "check": "measured",
110
+ "howMeasured": "WCAG formula on computed text and control colours versus effective background."
111
+ },
112
+ {
113
+ "id": "W12",
114
+ "medium": "web",
115
+ "name": "Semantic OKLCH tokens",
116
+ "threshold": "--color-bg/fg/accent/muted/border defined on :root in oklch(); component colour declarations resolve through var(--color-*)",
117
+ "unit": "",
118
+ "source": "W3C Design Tokens CG; Ottosson",
119
+ "check": "proxy",
120
+ "howMeasured": "Stylesheet: root custom properties present and in oklch(); share of colour declarations outside :root that use var()."
121
+ },
122
+ {
123
+ "id": "W13",
124
+ "medium": "web",
125
+ "name": "Five interaction states",
126
+ "threshold": "hover, focus-visible, active, disabled rules per interactive selector; focus ring ≥2 px, ≥3:1, not clipped",
127
+ "unit": "",
128
+ "source": "WCAG 2.4.11 / 2.4.13; Radix",
129
+ "check": "measured",
130
+ "howMeasured": "Stylesheet rules per interactive selector; after element.focus(), computed outline/box-shadow width, its contrast against adjacent colours, and whether its rect is clipped. Visibility under real conditions is ASSERTED."
131
+ },
132
+ {
133
+ "id": "W14",
134
+ "medium": "web",
135
+ "name": "Empty, loading and error states",
136
+ "threshold": "form error affordance present (aria-invalid styling, aria-describedby)",
137
+ "unit": "",
138
+ "source": "NN/g; Refactoring UI",
139
+ "check": "proxy",
140
+ "howMeasured": "Form error affordances measured; empty and loading states are ASSERTED."
141
+ },
142
+ {
143
+ "id": "W15",
144
+ "medium": "web",
145
+ "name": "Motion discipline",
146
+ "threshold": "transitions 150–300 ms; prefers-reduced-motion reset present; only transform and opacity animated; no autoplay without controls",
147
+ "unit": "ms",
148
+ "source": "Material motion; WCAG 2.2.2 / 2.3.3",
149
+ "check": "measured",
150
+ "howMeasured": "Transition durations, the reduced-motion media rule, animated properties in keyframes, video autoplay attributes."
151
+ },
152
+ {
153
+ "id": "W16",
154
+ "medium": "web",
155
+ "name": "Reserve space",
156
+ "threshold": "every image, video and iframe sized or given an aspect ratio; CLS reported as a lab number",
157
+ "unit": "",
158
+ "source": "web.dev Core Web Vitals",
159
+ "check": "proxy",
160
+ "howMeasured": "Size attributes and aspect-ratio measured; cumulative layout shift from a lab observer reported, not graded."
161
+ },
162
+ {
163
+ "id": "W17",
164
+ "medium": "web",
165
+ "name": "Performance budget",
166
+ "threshold": "≤2 font families; preload present; transfer size reported; LCP lab number reported; INP not measured",
167
+ "unit": "KB / ms",
168
+ "source": "web.dev; Addy Osmani",
169
+ "check": "proxy",
170
+ "howMeasured": "Font families and preload measured; transfer size and LCP reported from a lab run and labelled as such; INP is stated as not measured."
171
+ },
172
+ {
173
+ "id": "W18",
174
+ "medium": "web",
175
+ "name": "Landmarks, skip link, one h1",
176
+ "threshold": "header, nav, main, footer present; first focusable is the skip link; exactly one h1; no skipped heading levels; Tab order equals DOM order",
177
+ "unit": "",
178
+ "source": "WCAG 1.3.1 / 2.4.1",
179
+ "check": "measured",
180
+ "howMeasured": "Landmark elements, heading sequence, first focusable element, and the DOM-versus-visual order of focusable elements."
181
+ },
182
+ {
183
+ "id": "W19",
184
+ "medium": "web",
185
+ "name": "Forms",
186
+ "threshold": "every input labelled; type and autocomplete set; single column; error text linked to its field",
187
+ "unit": "",
188
+ "source": "Baymard; WCAG 3.3",
189
+ "check": "measured",
190
+ "howMeasured": "Label association, type and autocomplete attributes, input width versus form width, aria-describedby on invalid fields."
191
+ },
192
+ {
193
+ "id": "W20",
194
+ "medium": "web",
195
+ "name": "Meta layer",
196
+ "threshold": "title ≤60, description ≤155, og:image declared 1200×630, theme-color, favicon, JSON-LD parses",
197
+ "unit": "characters",
198
+ "source": "Open Graph; schema.org",
199
+ "check": "measured",
200
+ "howMeasured": "Document head read after load; JSON-LD parsed."
201
+ },
202
+ {
203
+ "id": "W21",
204
+ "medium": "web",
205
+ "name": "Show, do not only tell",
206
+ "threshold": "at least one non-decorative visual above the fold, and one visual per 2 content sections",
207
+ "unit": "visuals",
208
+ "source": "Ours, 2026-09-03",
209
+ "check": "measured",
210
+ "howMeasured": "Non-decorative images, inline SVG figures, video and embedded demos are counted per section from the rendered page; the logo and decorative marks are excluded. A page of pure text fails."
211
+ },
212
+ {
213
+ "id": "W22",
214
+ "medium": "web",
215
+ "name": "Evidence a stranger can check",
216
+ "threshold": "a proof section carrying at least one of: a named testimonial, a customer logo set, a screenshot of the thing working, or a specific number",
217
+ "unit": "",
218
+ "source": "Ours, 2026-09-03",
219
+ "check": "proxy",
220
+ "howMeasured": "Looks for a section marked proof, or a testimonial/figure/logo group, or a digit-bearing claim outside the nav and footer. Presence is measured; whether the evidence is TRUE or persuasive is not, and never claimed."
221
+ }
222
+ ]