design-lint: fourth-cycle simplifications and the pixel pass pinned without a browser

heroClasses reads the hero through heroIndexOf, check64 uses twMonoIsMaterial, check67Html imports capsRatio from chrome-eyebrow, sideStripes loses its bare block; a settle test drives confirmPixels with the swatch fixture (a claim dropped by the pixels behind it, one rewritten, a weak ring confirmed).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 09:26:03 +00:00
1 parent bd2a86fdf7
commit c0422db7f2
3 files changed
+47 -22

No files matched your search

+4 -10
View File
@@ -74,9 +74,7 @@ function sideStripes(ctx: LintContext, r: Reporter): void {
const parsed = col ? parseColor(col[0]) : null;
const ok = parsed ? oklchOf(parsed) : null;
if (parsed && ok && (parsed.alpha <= 0.05 || (ok.C < 0.04 && ok.L > 0.6))) continue;
{
r.report('template-cards', rule.file, d.line, `a ${m[1]}px coloured side stripe (${d.prop}: ${trunc(d.value, 32)}) on ${rule.selector}`, { fix: 'a hairline all round, no border, or an accent square' });
}
r.report('template-cards', rule.file, d.line, `a ${m[1]}px coloured side stripe (${d.prop}: ${trunc(d.value, 32)}) on ${rule.selector}`, { fix: 'a hairline all round, no border, or an accent square' });
}
}
}
@@ -109,13 +107,9 @@ function heroClasses(ctx: LintContext): Set<string> {
const out = new Set<string>();
for (const doc of ctx.docs) {
const h1 = doc.tags.findIndex((t) => !t.close && t.name === 'h1');
if (h1 < 0) continue;
for (let i = h1 - 1; i >= 0; i--) {
const t = doc.tags[i];
if (t.close || !/^(section|header)$/.test(t.name)) continue;
for (const c of (getAttr(t.attrs, 'class') || '').split(/\s+/)) if (c) out.add(c);
break;
}
const hero = h1 < 0 ? -1 : heroIndexOf(doc, h1);
if (hero < 0) continue;
for (const c of (getAttr(doc.tags[hero].attrs, 'class') || '').split(/\s+/)) if (c) out.add(c);
}
return out;
}
+4 -12
View File
@@ -11,6 +11,8 @@ import { GENERIC_FAMILIES, HEADING_SEL, LABEL_PX, MONO_FACE, TRACKED_EM, famKey,
import { ancestorsOf, closeIndex, getAttr, innerOf, openTags, plainText, stripTags, textOf } from '../html';
import type { Reporter } from '../report';
import { trunc } from '../report';
import { twMonoIsMaterial } from '../tailwind';
import { capsRatio } from './chrome-eyebrow';
/** A selector without its pseudo-classes and pseudo-elements, whitespace folded. */
const baseSelector = (sel: string): string => sel.replace(/::?[a-z-]+(\([^)]*\))?/gi, '').replace(/\s+/g, ' ').trim();
@@ -223,15 +225,9 @@ function check55(ctx: LintContext, r: Reporter): void {
}
// sloplint monoIsMaterial (gate 64): a monospace face declared as the display, body or outlier is the page's material, not a costume
function monoIsMaterial(ctx: LintContext): boolean {
const mono = (v: string | undefined) => !!v && MONO_FACE.test(firstFamily(resolveVars(v, ctx.tokens)));
return mono(ctx.tokens['--font-display']) || mono(ctx.tokens['--font-body']) || mono(ctx.tokens['--font-outlier']);
}
// sloplint check64 (gate 64)
// sloplint check64 (gate 64); twMonoIsMaterial is sloplint's monoIsMaterial: a mono face declared as the display, body or outlier is the page's material, not a costume
function check64(ctx: LintContext, r: Reporter): void {
if (monoIsMaterial(ctx)) return;
if (twMonoIsMaterial(ctx.tokens)) return;
const monoFam = (v: string) => MONO_FACE.test(firstFamily(resolveVars(v, ctx.tokens)));
const LABELISH = /label|caption|meta|eyebrow|kicker|tag\b|badge|chip|small|folio|footnote|legend|\bth\b|thead|figcaption|overline|dateline|byline|credit|note\b/i;
const DATAISH = /code|pre\b|kbd|samp|num|figure|stat|price|value|data|tabular|counter|timer|clock|coord|amount|total|score|version|hash|\bid\b|sku|time|date|hour|utc|eta|lot\b|edition|ref\b|dimension|measure|reading|plate-?no|tag-?no|url|uri\b|path|endpoint|command|cmd|snippet|install/i;
@@ -371,10 +367,6 @@ function check67(ctx: LintContext, r: Reporter): void {
}
const CAPS_TAGS = /^(p|span|small|a|li|div|figcaption|td|th|dt|dd|label|time|cite|strong|b|em|button|summary)$/;
const capsRatio = (s: string): number => {
const letters = s.replace(/[^A-Za-z]/g, '');
return letters ? (letters.match(/[A-Z]/g) || []).length / letters.length : 0;
};
// sloplint check67Html (gate 67), the markup half: text typed in capitals at label size or tracked
function check67Html(ctx: LintContext, r: Reporter): void {
+39
View File
@@ -258,3 +258,42 @@ describe('design-lint fourth cycle: never-waivable classes refuse by class befor
expect(applyWaivers(measured.findings, set).map((f) => [f.rule, f.grade])).toEqual([['nested-cards', 'blocks'], ['waiver', 'ask'], ['waiver', 'ask'], ['waiver', 'ask']]);
});
});
describe('design-lint fourth cycle: the pixel pass with a readable screenshot', () => {
const PAGE = HEAD + '</head><body><main><h1>t</h1><p class="faint">Body.</p><a href="#">Go</a></main></body></html>';
const SWATCH = path.join(path.resolve(import.meta.dir, '..'), 'test', 'fixtures', 'design-lint', 'render', 'swatch.png');
const SETUP = 1, FIRST = 2, FOCUS = 13;
test('a contrast claim is dropped when the pixels behind it clear the threshold, rewritten when they confirm it, and a weak ring is confirmed from the pixels around it', async () => {
// swatch.png is 8x8, red on the left half and blue on the right; the page rects are in CSS pixels, scaled to the screenshot by its width
const dir = tree({ 'index.html': PAGE });
const file = path.join(dir, 'index.html');
const white = { rule: 'low-contrast', grade: 'fail', evidence: 'computed contrast 2.50:1 under the 4.5:1 threshold on h1 (2.50:1)', probe: { path: 'main>h1', fg: [1, 1, 1], threshold: 4.5 } };
const grey = { rule: 'low-contrast', grade: 'fail', evidence: 'computed contrast 2.50:1 under the 4.5:1 threshold on p.faint (2.50:1)', probe: { path: 'main>p', fg: [120 / 255, 120 / 255, 120 / 255], threshold: 4.5 } };
const ring = { key: 'a', path: 'main>a', at: { tag: 'a', text: 'Go' }, bare: false, ratio: 1.2, ring: [1, 1, 1], pad: 2 };
const shots: string[] = [];
const run: Runner = async (spec: RenderSpec): Promise<RenderResult> => {
const evals: Record<number, string> = {};
const shot = spec.steps.findIndex((s) => s.kind === 'screenshot');
if (shot < 0) {
evals[SETUP] = '{"lost":[]}';
for (let i = FIRST; i <= FOCUS; i++) evals[i] = '[]';
evals[FIRST + 5] = JSON.stringify([white, grey]);
evals[FOCUS] = JSON.stringify([ring]);
return { ok: true, engine: 'browse', outputs: [], evals, stdout: '' };
}
spec.steps.forEach((s, i) => {
if (s.kind !== 'screenshot') return;
fs.copyFileSync(SWATCH, s.out);
shots.push(path.basename(s.out));
const width = s.width ?? 0, half = width / 2;
evals[i - 1] = s.out.endsWith('text.png') ? JSON.stringify([{ x: 0, y: 0, w: half, h: width }, { x: half, y: 0, w: half, h: width }]) : JSON.stringify([{ x: 0, y: 0, w: half, h: width }]);
});
return { ok: true, engine: 'browse', outputs: [], evals, stdout: '' };
};
const r = await lintRender([file], { cwd: dir, run });
expect(shots.sort()).toEqual(['ring.png', 'text.png']);
expect(only(r.findings, 'low-contrast').map((f) => f.evidence)).toEqual(['[1280px] contrast 2.29:1 against the pixels behind, under the 4.5:1 threshold on p.faint (2.29:1)']);
expect(only(r.findings, 'missing-focus-visible')).toEqual([]);
});
});