mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 09:36:59 +03:00
Port: Fix detector URL scans and advisory handling (#709)
Upstream sha fa44839f72.
Advisory handling. `severity` becomes the canonical registry field: the
`advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on
`severity == "advisory"`, and `derive_advisory_flag` stamps the finding's
`advisory: true` from the effective severity, so a per-finding promotion or
demotion carries the flag. The html and browser engines call it after their
severity override; the detect CLI and the hook accept either spelling; the
driver's serializer and the wasm registry exports derive it the same way.
em-dash-overuse moves from `advisory: true` to `severity: "advisory"`.
URL scans. `expand_joined_url_targets` splits an argv value that is entirely
whitespace-separated URLs and leaves paths with spaces alone. The browser
driver reads the readable linked-stylesheet corpus into the HTML pattern
corpora and resolves a finding's selector with `selector_nodes_for_live_dom`
/ `pseudo_element_host_selector`, so an unresolvable selector drops the
finding instead of keeping it page-level. The CSSOM walk itself is page JS:
`browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping
rules flattened, container-query probes, effective keyframes) and puts it in
the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page
route, and the Dom trait carries `linked_stylesheet_text`.
Also `enclosing_css_selector` blanks comments before hunting the previous
declaration delimiter, and `check_typography` reports the uniquely most-used
family instead of every family over a 15% share.
Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns`
is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree
over the shared corpus (432 findings). The two changed lines in
tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were
re-recorded by running origin/main's `checkHtmlPatterns` over the frozen
args; only the comment-polluted selector changed. Goldens re-recorded for
the advisory partition (config-*, fixture gemini/gpt-tells,
numbered-section-labels, scoped-ignore, shape-assembled-illustration,
color, em-dash-entities) and the help text, each cross-checked against the
JS on origin/main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
66482a9808
commit
8ac3886a9c
@@ -101,6 +101,11 @@ const __impeccableDom = {
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
linked_stylesheet_text() {
|
||||
// The CSSOM walk lives in 15-snapshot.js so the standalone snapshot
|
||||
// producer carries it too; both routes read the same corpus.
|
||||
return __snapLinkedStylesheetText();
|
||||
},
|
||||
document_html_for_patterns() {
|
||||
const docClone = document.documentElement.cloneNode(true);
|
||||
for (const node of docClone.querySelectorAll('[id^="impeccable-live-"]')) node.remove();
|
||||
|
||||
@@ -90,6 +90,327 @@ function __snapDirectTextRect(node) {
|
||||
return [left, top, right - left, bottom - top];
|
||||
}
|
||||
|
||||
// ─── Linked stylesheet corpus (JS: injected/index.mjs #709) ────────────────
|
||||
|
||||
// JS: injected/index.mjs#pseudoElementHostSelector
|
||||
function __snapPseudoElementHostSelector(selector) {
|
||||
const raw = String(selector || '');
|
||||
const legacyNames = new Set(['before', 'after', 'first-letter', 'first-line']);
|
||||
const isNameChar = char => /[a-zA-Z0-9_-]/.test(char || '');
|
||||
const consumeFunction = (start) => {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
for (let i = start; i < raw.length; i += 1) {
|
||||
const char = raw[i];
|
||||
if (char === '\\') { i += 1; continue; }
|
||||
if (quote) { if (char === quote) quote = ''; continue; }
|
||||
if (char === '"' || char === "'") { quote = char; continue; }
|
||||
if (char === '(') depth += 1;
|
||||
if (char === ')' && --depth === 0) return i + 1;
|
||||
}
|
||||
return raw.length;
|
||||
};
|
||||
|
||||
let output = '';
|
||||
let found = false;
|
||||
for (let i = 0; i < raw.length;) {
|
||||
const char = raw[i];
|
||||
if (char === '\\') {
|
||||
output += raw.slice(i, Math.min(raw.length, i + 2));
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
const quote = char;
|
||||
const start = i;
|
||||
i += 1;
|
||||
while (i < raw.length) {
|
||||
if (raw[i] === '\\') { i += 2; continue; }
|
||||
const value = raw[i];
|
||||
i += 1;
|
||||
if (value === quote) break;
|
||||
}
|
||||
output += raw.slice(start, i);
|
||||
continue;
|
||||
}
|
||||
if (char !== ':') { output += char; i += 1; continue; }
|
||||
|
||||
let end = i + 1;
|
||||
let isPseudoElement = false;
|
||||
if (raw[end] === ':') {
|
||||
end += 1;
|
||||
const nameStart = end;
|
||||
while (isNameChar(raw[end])) end += 1;
|
||||
isPseudoElement = end > nameStart;
|
||||
} else {
|
||||
const nameStart = end;
|
||||
while (isNameChar(raw[end])) end += 1;
|
||||
isPseudoElement = legacyNames.has(raw.slice(nameStart, end).toLowerCase());
|
||||
}
|
||||
if (!isPseudoElement) { output += char; i += 1; continue; }
|
||||
if (raw[end] === '(') end = consumeFunction(end);
|
||||
found = true;
|
||||
if (!output || /[\s>+~,]/.test(output[output.length - 1])) output += '*';
|
||||
i = end;
|
||||
}
|
||||
if (!found) return null;
|
||||
return output.trim().replace(/,\s*(?=,|$)/g, '');
|
||||
}
|
||||
|
||||
// JS: injected/index.mjs#selectorNodesForLiveDom
|
||||
function __snapSelectorNodesForLiveDom(root, selector) {
|
||||
const raw = String(selector || '').trim();
|
||||
if (!raw) return null;
|
||||
const fallback = __snapPseudoElementHostSelector(raw);
|
||||
if (fallback == null) {
|
||||
// An empty result from a valid full selector is authoritative. In
|
||||
// particular, do not broaden inactive :hover/:focus/:not() rules to
|
||||
// their host element by stripping pseudo-classes.
|
||||
try { return Array.from(root.querySelectorAll(raw)); }
|
||||
catch { return null; }
|
||||
}
|
||||
// Resolve pseudo-elements to their originating live elements. An attached
|
||||
// pseudo-element (`.card::before`) belongs to the element before it, while
|
||||
// a hostless pseudo-element after a combinator (`main > ::before`) belongs
|
||||
// to a matching element at that position (`main > *`).
|
||||
if (!fallback || /^[,\s]*$/.test(fallback)) return null;
|
||||
try { return Array.from(root.querySelectorAll(fallback)); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
let __snapContainerProbeSequence = 0;
|
||||
|
||||
function __snapIsContainerCssRule(rule) {
|
||||
return rule?.constructor?.name === 'CSSContainerRule'
|
||||
|| /^\s*@container\b/i.test(rule?.cssText || '');
|
||||
}
|
||||
|
||||
function __snapStyleRuleAppliesToLiveMatches(rule, matches) {
|
||||
const style = rule?.style;
|
||||
if (!style || !matches?.length || typeof getComputedStyle !== 'function') return false;
|
||||
const sequence = ++__snapContainerProbeSequence;
|
||||
const property = `--impeccable-container-probe-${sequence}-${Math.random().toString(36).slice(2)}`;
|
||||
const value = `impeccable-container-active-${sequence}`;
|
||||
const previousValue = style.getPropertyValue(property);
|
||||
const previousPriority = style.getPropertyPriority(property);
|
||||
try { style.setProperty(property, value, 'important'); }
|
||||
catch { return false; }
|
||||
|
||||
const pseudoElements = [...new Set(
|
||||
String(rule.selectorText || '').match(/::[a-zA-Z-]+(?:\([^)]*\))?/g) || [],
|
||||
)];
|
||||
try {
|
||||
return matches.some(el => [null, ...pseudoElements].some(pseudo => {
|
||||
try {
|
||||
const computed = pseudo ? getComputedStyle(el, pseudo) : getComputedStyle(el);
|
||||
return computed.getPropertyValue(property).trim() === value;
|
||||
} catch { return false; }
|
||||
}));
|
||||
} finally {
|
||||
if (previousValue) style.setProperty(property, previousValue, previousPriority);
|
||||
else style.removeProperty(property);
|
||||
}
|
||||
}
|
||||
|
||||
function __snapConditionalCssRuleIsActive(rule) {
|
||||
const type = Number(rule?.type);
|
||||
const constructorName = rule?.constructor?.name || '';
|
||||
if (constructorName === 'CSSMediaRule' || type === 4) {
|
||||
const condition = rule.conditionText || rule.media?.mediaText || '';
|
||||
if (!condition || typeof window.matchMedia !== 'function') return true;
|
||||
try { return window.matchMedia(condition).matches; }
|
||||
catch { return true; }
|
||||
}
|
||||
if (constructorName === 'CSSSupportsRule' || type === 12) {
|
||||
const condition = rule.conditionText || '';
|
||||
if (!condition || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return true;
|
||||
try { return CSS.supports(condition); }
|
||||
catch { return true; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function __snapSplitCssCommaList(value) {
|
||||
const parts = [];
|
||||
let current = '';
|
||||
let quote = '';
|
||||
let escaped = false;
|
||||
for (const char of String(value || '')) {
|
||||
if (escaped) { current += char; escaped = false; continue; }
|
||||
if (char === '\\') { current += char; escaped = true; continue; }
|
||||
if (quote) { current += char; if (char === quote) quote = ''; continue; }
|
||||
if (char === '"' || char === "'") { quote = char; current += char; continue; }
|
||||
if (char === ',') { parts.push(current); current = ''; continue; }
|
||||
current += char;
|
||||
}
|
||||
parts.push(current);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function __snapNormalizeAnimationName(value) {
|
||||
const name = String(value || '').trim();
|
||||
if (name.length >= 2 && name[0] === name[name.length - 1] && (name[0] === '"' || name[0] === "'")) {
|
||||
return name.slice(1, -1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function __snapAnimationNamesDeclaredByRule(rule) {
|
||||
const style = rule?.style;
|
||||
if (!style) return [];
|
||||
let value = '';
|
||||
try {
|
||||
value = style.animationName
|
||||
|| style.getPropertyValue?.('animation-name')
|
||||
|| style.webkitAnimationName
|
||||
|| style.getPropertyValue?.('-webkit-animation-name')
|
||||
|| '';
|
||||
} catch { return []; }
|
||||
return __snapSplitCssCommaList(value)
|
||||
.map(__snapNormalizeAnimationName)
|
||||
.filter(name => name && name.toLowerCase() !== 'none');
|
||||
}
|
||||
|
||||
function __snapKeyframesRuleName(rule, cssText) {
|
||||
const constructorName = rule?.constructor?.name || '';
|
||||
const type = Number(rule?.type);
|
||||
const isKeyframes = constructorName === 'CSSKeyframesRule'
|
||||
|| constructorName === 'WebKitCSSKeyframesRule'
|
||||
|| type === 7
|
||||
|| /^\s*@(?:-webkit-)?keyframes\b/i.test(cssText);
|
||||
if (!isKeyframes) return '';
|
||||
const match = String(cssText || '').match(/^\s*@(?:-webkit-)?keyframes\s+([^\s{]+)/i);
|
||||
return __snapNormalizeAnimationName(rule?.name || match?.[1] || '');
|
||||
}
|
||||
|
||||
function __snapCssPropertyName(property) {
|
||||
if (property.startsWith('--')) return property;
|
||||
return property.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
function __snapResolvedAnimationKeyframes(candidateNames) {
|
||||
if (typeof document.getAnimations !== 'function') return null;
|
||||
let animations;
|
||||
try { animations = document.getAnimations(); }
|
||||
catch { return null; }
|
||||
|
||||
const resolved = new Map();
|
||||
const metadata = new Set(['offset', 'computedOffset', 'easing', 'composite']);
|
||||
for (const animation of animations) {
|
||||
const name = __snapNormalizeAnimationName(animation?.animationName || '');
|
||||
if (!name || !candidateNames.has(name) || resolved.has(name)) continue;
|
||||
let frames;
|
||||
try { frames = animation.effect?.getKeyframes?.() || []; }
|
||||
catch { continue; }
|
||||
const blocks = [];
|
||||
for (const frame of frames) {
|
||||
const rawOffset = Number.isFinite(frame.computedOffset) ? frame.computedOffset : frame.offset;
|
||||
if (!Number.isFinite(rawOffset)) continue;
|
||||
const offset = Math.round(rawOffset * 1000000) / 10000;
|
||||
const declarations = Object.entries(frame)
|
||||
.filter(([property, value]) => !metadata.has(property) && value != null && value !== '')
|
||||
.map(([property, value]) => `${__snapCssPropertyName(property)}: ${value};`);
|
||||
const easing = String(frame.easing || '').trim();
|
||||
if (easing && easing.toLowerCase() !== 'linear') {
|
||||
declarations.push(`animation-timing-function: ${easing};`);
|
||||
}
|
||||
if (declarations.length === 0) continue;
|
||||
blocks.push(`${offset}% { ${declarations.join(' ')} }`);
|
||||
}
|
||||
if (blocks.length > 0) resolved.set(name, `@keyframes ${name} { ${blocks.join(' ')} }`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Read CSS that is absent from document.outerHTML. Inline <style> blocks are
|
||||
// already present in the HTML pattern corpus, so limit this walk to linked
|
||||
// stylesheets. Flatten grouping rules so each declaration keeps its selector,
|
||||
// and admit only selector rules that target the live DOM. That prevents
|
||||
// unused utilities from feeding both selector-scoped and page-level checks.
|
||||
// Same-origin CSS and readable CORS sheets participate; browser security
|
||||
// exceptions for cross-origin sheets are expected and skipped.
|
||||
// JS: injected/index.mjs#linkedStylesheetText
|
||||
function __snapLinkedStylesheetText() {
|
||||
const parts = [];
|
||||
const seen = new Set();
|
||||
const animationNames = new Set();
|
||||
const keyframeCandidates = new Map();
|
||||
const appendRules = (rules, requiresAppliedMatch = false) => {
|
||||
for (const rule of rules) {
|
||||
if (rule.styleSheet) { appendSheet(rule.styleSheet); continue; }
|
||||
const cssText = rule.cssText || '';
|
||||
if (rule.selectorText) {
|
||||
const matches = __snapSelectorNodesForLiveDom(document, rule.selectorText);
|
||||
// Only declarations with a resolvable live host enter the corpus.
|
||||
// Unresolvable selectors are uncertain, not evidence that a pattern
|
||||
// rendered, and retaining them would leak unused CSS into findings.
|
||||
if (
|
||||
matches?.length > 0
|
||||
&& (!requiresAppliedMatch || __snapStyleRuleAppliesToLiveMatches(rule, matches))
|
||||
) {
|
||||
parts.push(cssText);
|
||||
for (const name of __snapAnimationNamesDeclaredByRule(rule)) animationNames.add(name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let nested = [];
|
||||
let hasNestedRules = false;
|
||||
try {
|
||||
const ruleList = rule.cssRules;
|
||||
hasNestedRules = ruleList != null;
|
||||
nested = Array.from(ruleList || []);
|
||||
} catch { continue; }
|
||||
const keyframesName = __snapKeyframesRuleName(rule, cssText);
|
||||
if (keyframesName) {
|
||||
// Keyframes do not merge: when a name is defined more than once, the
|
||||
// later effective definition replaces the earlier one.
|
||||
keyframeCandidates.set(keyframesName, { name: keyframesName, cssText });
|
||||
continue;
|
||||
}
|
||||
if (hasNestedRules) {
|
||||
if (!__snapConditionalCssRuleIsActive(rule)) continue;
|
||||
appendRules(nested, requiresAppliedMatch || __snapIsContainerCssRule(rule));
|
||||
continue;
|
||||
}
|
||||
// Other selector-less leaf at-rules cannot be tied to a rendered node.
|
||||
}
|
||||
};
|
||||
const appendSheet = (sheet) => {
|
||||
if (!sheet || seen.has(sheet)) return;
|
||||
seen.add(sheet);
|
||||
let rules;
|
||||
try { rules = Array.from(sheet.cssRules || sheet.rules || []); }
|
||||
catch { return; }
|
||||
appendRules(rules);
|
||||
};
|
||||
let sheets;
|
||||
try { sheets = Array.from(document.styleSheets || []); }
|
||||
catch { return ''; }
|
||||
for (const sheet of sheets) {
|
||||
const owner = sheet.ownerNode;
|
||||
if (owner?.tagName?.toLowerCase() !== 'link') continue;
|
||||
if (!/\bstylesheet\b/i.test(owner.getAttribute?.('rel') || '')) continue;
|
||||
appendSheet(sheet);
|
||||
}
|
||||
// Motion checks need the effective body of a live animation's keyframes.
|
||||
// Let the browser resolve duplicate names across source order, imports,
|
||||
// conditional groups, and cascade layers, then serialize those computed
|
||||
// frames back into the pattern corpus. Browsers also make container-nested
|
||||
// keyframes globally available, so lexical grouping is not a reliable
|
||||
// activity signal. When the Web Animations API is unavailable, fall back to
|
||||
// the last source-order definition referenced by a retained linked rule.
|
||||
const resolvedKeyframes = __snapResolvedAnimationKeyframes(new Set(keyframeCandidates.keys()));
|
||||
if (resolvedKeyframes) {
|
||||
parts.push(...resolvedKeyframes.values());
|
||||
} else {
|
||||
for (const candidate of keyframeCandidates.values()) {
|
||||
if (!animationNames.has(candidate.name)) continue;
|
||||
parts.push(candidate.cssText);
|
||||
}
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
// Every @keyframes rule, in document.styleSheets order (nested rules walked
|
||||
// breadth-first like 10-probe.js keyframes()); first rule per name wins.
|
||||
function __snapKeyframes() {
|
||||
@@ -343,6 +664,7 @@ const __impeccableSnapshot = {
|
||||
scrollY: window.scrollY,
|
||||
html: docClone.outerHTML,
|
||||
keyframes: __snapKeyframes(),
|
||||
linkedCss: __snapLinkedStylesheetText(),
|
||||
styleProps: __SNAP_STYLE_PROPS,
|
||||
pseudoProps: __SNAP_PSEUDO_PROPS,
|
||||
strings,
|
||||
|
||||
@@ -368,6 +368,7 @@ fn detect_url_impl(
|
||||
if !r.severity.is_empty() && r.severity != item.severity {
|
||||
item.severity = r.severity;
|
||||
}
|
||||
impeccable_core::findings::derive_advisory_flag(&mut item);
|
||||
findings.push(item);
|
||||
}
|
||||
Ok(findings)
|
||||
|
||||
@@ -556,6 +556,10 @@ fn drop_dangling_commas(s: &str) -> String {
|
||||
/// The live-DOM scope of a CSS-text finding's selector: pseudo segments
|
||||
/// stripped, dangling commas removed. `None` when nothing queryable is left
|
||||
/// (the finding stays page-level).
|
||||
///
|
||||
/// Superseded by [`pseudo_element_host_selector`] for the driver's own
|
||||
/// filter (#709); kept because the static engine still spells the scope this
|
||||
/// way.
|
||||
pub fn html_pattern_query(selector: &str) -> Option<String> {
|
||||
let stripped = PSEUDO_SEGMENT_RE.replace_all(selector, "");
|
||||
let query = drop_dangling_commas(crate::js::trim(&stripped));
|
||||
@@ -565,6 +569,155 @@ pub fn html_pattern_query(selector: &str) -> Option<String> {
|
||||
Some(query)
|
||||
}
|
||||
|
||||
fn is_selector_name_char(c: Option<char>) -> bool {
|
||||
matches!(c, Some(c) if c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
}
|
||||
|
||||
/// JS: injected/index.mjs#pseudoElementHostSelector
|
||||
///
|
||||
/// Rewrites a selector so its pseudo-elements resolve to the live element
|
||||
/// that originates them: `.card::before` to `.card`, and a hostless
|
||||
/// `main > ::before` to `main > *`. `None` when the selector carries no
|
||||
/// pseudo-element at all, which is the caller's signal that the full
|
||||
/// selector is queryable as written.
|
||||
///
|
||||
/// JS-PARITY: the JS indexes UTF-16 code units; this walks chars, which
|
||||
/// differs only for an astral character inside a selector literal.
|
||||
pub fn pseudo_element_host_selector(selector: &str) -> Option<String> {
|
||||
const LEGACY_NAMES: &[&str] = &["before", "after", "first-letter", "first-line"];
|
||||
let raw: Vec<char> = selector.chars().collect();
|
||||
let consume_function = |start: usize| -> usize {
|
||||
let mut depth: i32 = 0;
|
||||
let mut quote: Option<char> = None;
|
||||
let mut i = start;
|
||||
while i < raw.len() {
|
||||
let ch = raw[i];
|
||||
if ch == '\\' {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if let Some(q) = quote {
|
||||
if ch == q {
|
||||
quote = None;
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if ch == '"' || ch == '\'' {
|
||||
quote = Some(ch);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if ch == '(' {
|
||||
depth += 1;
|
||||
}
|
||||
if ch == ')' {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
raw.len()
|
||||
};
|
||||
|
||||
let mut output = String::new();
|
||||
let mut found = false;
|
||||
let mut i = 0usize;
|
||||
while i < raw.len() {
|
||||
let ch = raw[i];
|
||||
if ch == '\\' {
|
||||
let end = raw.len().min(i + 2);
|
||||
output.extend(&raw[i..end]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if ch == '"' || ch == '\'' {
|
||||
let quote = ch;
|
||||
let start = i;
|
||||
i += 1;
|
||||
while i < raw.len() {
|
||||
if raw[i] == '\\' {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
let value = raw[i];
|
||||
i += 1;
|
||||
if value == quote {
|
||||
break;
|
||||
}
|
||||
}
|
||||
output.extend(&raw[start..raw.len().min(i)]);
|
||||
continue;
|
||||
}
|
||||
if ch != ':' {
|
||||
output.push(ch);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut end = i + 1;
|
||||
let is_pseudo_element;
|
||||
if raw.get(end) == Some(&':') {
|
||||
end += 1;
|
||||
let name_start = end;
|
||||
while is_selector_name_char(raw.get(end).copied()) {
|
||||
end += 1;
|
||||
}
|
||||
is_pseudo_element = end > name_start;
|
||||
} else {
|
||||
let name_start = end;
|
||||
while is_selector_name_char(raw.get(end).copied()) {
|
||||
end += 1;
|
||||
}
|
||||
let name: String = raw[name_start..end].iter().collect();
|
||||
is_pseudo_element = LEGACY_NAMES.contains(&crate::js::to_lower_case(&name).as_str());
|
||||
}
|
||||
if !is_pseudo_element {
|
||||
output.push(ch);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if raw.get(end) == Some(&'(') {
|
||||
end = consume_function(end);
|
||||
}
|
||||
found = true;
|
||||
let last = output.chars().last();
|
||||
if last.is_none()
|
||||
|| matches!(last, Some(c) if crate::js::is_js_whitespace(c)
|
||||
|| c == '>' || c == '+' || c == '~' || c == ',')
|
||||
{
|
||||
output.push('*');
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
if !found {
|
||||
return None;
|
||||
}
|
||||
Some(drop_dangling_commas(crate::js::trim(&output)))
|
||||
}
|
||||
|
||||
/// JS: injected/index.mjs#selectorNodesForLiveDom
|
||||
///
|
||||
/// `None` means "unresolvable": the DOM API refused the selector, or the
|
||||
/// pseudo-element rewrite left nothing queryable. An empty vector from a
|
||||
/// selector the DOM did accept is authoritative, so an inactive
|
||||
/// `:hover` / `:focus` / `:not()` rule is never broadened to its host.
|
||||
pub fn selector_nodes_for_live_dom(dom: &dyn Dom, selector: &str) -> Option<Vec<ElId>> {
|
||||
let raw = crate::js::trim(selector);
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Some(fallback) = pseudo_element_host_selector(raw) else {
|
||||
return dom.query_all(None, raw).ok();
|
||||
};
|
||||
if fallback.is_empty() || ONLY_COMMAS_WS_RE.is_match(&fallback) {
|
||||
return None;
|
||||
}
|
||||
dom.query_all(None, &fallback).ok()
|
||||
}
|
||||
|
||||
/// The regex-on-HTML pass of collectBrowserFindings: `checkHtmlPatterns` on
|
||||
/// the live document's HTML, selector-scoped filtering against the live DOM
|
||||
/// (a selector matching nothing drops the finding; a match under a
|
||||
@@ -573,22 +726,26 @@ pub fn html_pattern_query(selector: &str) -> Option<String> {
|
||||
/// caller applies `_ruleOk`.
|
||||
pub fn scoped_html_pattern_findings(dom: &dyn Dom) -> Vec<BrowserFinding> {
|
||||
let html = dom.document_html_for_patterns();
|
||||
let all = crate::checks::html_patterns::check_html_patterns(&html, None);
|
||||
// Linked stylesheets are absent from the page's outerHTML, so the probe
|
||||
// hands their readable, live-resolving rules to the style corpus (#709).
|
||||
let mut corpora = crate::checks::html_patterns::build_html_pattern_corpora(&html);
|
||||
let linked_css = dom.linked_stylesheet_text();
|
||||
if !linked_css.is_empty() {
|
||||
corpora.style_text.push('\n');
|
||||
corpora.style_text.push_str(&linked_css);
|
||||
}
|
||||
let all = crate::checks::html_patterns::check_html_patterns(&html, Some(&corpora));
|
||||
let mut out = Vec::new();
|
||||
for f in all {
|
||||
if let Some(selector) = f.selector.as_deref().filter(|s| !s.is_empty()) {
|
||||
if let Some(query) = html_pattern_query(selector) {
|
||||
match dom.query_all(None, &query) {
|
||||
Err(_) => {}
|
||||
Ok(matches) => {
|
||||
if matches.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !matches.iter().any(|el| !scoped_ignore_active(dom, *el, &f.id)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(matches) = selector_nodes_for_live_dom(dom, selector) else {
|
||||
continue;
|
||||
};
|
||||
if matches.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !matches.iter().any(|el| !scoped_ignore_active(dom, *el, &f.id)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let mut item = BrowserFinding::new(f.id.clone(), f.snippet.clone());
|
||||
@@ -657,11 +814,12 @@ pub fn serialize_findings(dom: &dyn Dom, groups: &[FindingGroup]) -> serde_json:
|
||||
"category".into(),
|
||||
Value::String(ap.map(|a| a.category).unwrap_or("quality").to_string()),
|
||||
);
|
||||
// Per-finding promotions override the registry default, so
|
||||
// derive the advisory flag strictly from the effective
|
||||
// severity (#709).
|
||||
let advisory = severity == "advisory";
|
||||
m.insert("severity".into(), Value::String(severity));
|
||||
m.insert(
|
||||
"advisory".into(),
|
||||
Value::Bool(ap.map_or(false, |a| a.advisory)),
|
||||
);
|
||||
m.insert("advisory".into(), Value::Bool(advisory));
|
||||
m.insert("detail".into(), Value::String(f.detail.clone()));
|
||||
m.insert(
|
||||
"ignoreValue".into(),
|
||||
@@ -1298,3 +1456,40 @@ mod tests {
|
||||
assert_eq!(visual_contrast_result_el(&d, &pass), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pseudo_host_tests {
|
||||
use super::pseudo_element_host_selector as host;
|
||||
|
||||
#[test]
|
||||
fn pseudo_element_hosts() {
|
||||
// No pseudo-element: the full selector stays queryable as written.
|
||||
assert_eq!(host(".card"), None);
|
||||
assert_eq!(host("a:hover"), None);
|
||||
assert_eq!(host("li:not(.x)"), None);
|
||||
// Attached and hostless pseudo-elements.
|
||||
assert_eq!(host(".card::before"), Some(".card".to_string()));
|
||||
assert_eq!(host("main > ::before"), Some("main > *".to_string()));
|
||||
assert_eq!(host("::after"), Some("*".to_string()));
|
||||
// The legacy one-colon spellings only.
|
||||
assert_eq!(host(".c:before"), Some(".c".to_string()));
|
||||
assert_eq!(host(".c:focus"), None);
|
||||
// Functional pseudo-elements consume their argument list.
|
||||
assert_eq!(host("p::part(label) span"), Some("p span".to_string()));
|
||||
// Literals are preserved, colons inside them are not pseudo starts.
|
||||
assert_eq!(host("[data-x=\"a::b\"]"), None);
|
||||
assert_eq!(
|
||||
host("[data-x=\"a::b\"]::before"),
|
||||
Some("[data-x=\"a::b\"]".to_string())
|
||||
);
|
||||
// A pseudo-class keeps its colon while a pseudo-element resolves.
|
||||
assert_eq!(host("a:hover::after"), Some("a:hover".to_string()));
|
||||
// Values recorded from the JS on origin/main (#709).
|
||||
assert_eq!(host(".a::before, .b"), Some(".a, .b".to_string()));
|
||||
assert_eq!(host(".a::before,"), Some(".a".to_string()));
|
||||
assert_eq!(host("::before ::after"), Some("* *".to_string()));
|
||||
assert_eq!(host(r"\:esc::before"), Some(r"\:esc".to_string()));
|
||||
assert_eq!(host("a::before("), Some("a".to_string()));
|
||||
assert_eq!(host("div::first-line"), Some("div".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,27 +141,30 @@ pub fn check_typography(dom: &dyn Dom) -> Vec<BrowserFinding> {
|
||||
}
|
||||
|
||||
if total_text_elements >= 20.0 {
|
||||
const PRIMARY_THRESHOLD: f64 = 0.15;
|
||||
// Report the actual primary face: the uniquely most-used family. The
|
||||
// old 15% threshold labeled secondary faces as primary, e.g. an 82/18
|
||||
// split (#709). `Array.prototype.sort` is stable, so ties keep
|
||||
// first-seen order and the tie test compares the top two counts.
|
||||
let hostname = dom.hostname();
|
||||
for (font, count) in &font_usage {
|
||||
let share = count / total_text_elements;
|
||||
if share < PRIMARY_THRESHOLD {
|
||||
continue;
|
||||
let mut ranked: Vec<&(String, f64)> = font_usage.iter().collect();
|
||||
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
if let Some((font, count)) = ranked.first().map(|(f, c)| (f, *c)) {
|
||||
let tied = ranked.get(1).map(|r| r.1) == Some(count);
|
||||
if !tied {
|
||||
let share = count / total_text_elements;
|
||||
if OVERUSED_FONTS.contains(&font.as_str())
|
||||
&& !is_brand_font_on_own_domain(font, Some(&hostname))
|
||||
{
|
||||
findings.push(BrowserFinding::new(
|
||||
"overused-font",
|
||||
format!(
|
||||
"Primary font: {} ({}% of text)",
|
||||
font,
|
||||
number_to_string(math_round(share * 100.0))
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
if !OVERUSED_FONTS.contains(&font.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if is_brand_font_on_own_domain(font, Some(&hostname)) {
|
||||
continue;
|
||||
}
|
||||
findings.push(BrowserFinding::new(
|
||||
"overused-font",
|
||||
format!(
|
||||
"Primary font: {} ({}% of text)",
|
||||
font,
|
||||
number_to_string(math_round(share * 100.0))
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ static ROWS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("warning"),
|
||||
advisory: false,
|
||||
name: "Unfinished copy marker",
|
||||
description: "Text still carries a TODO marker from drafting.",
|
||||
skill_section: None,
|
||||
@@ -27,7 +26,6 @@ static ROWS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("warning"),
|
||||
advisory: false,
|
||||
name: "Page ships an unfinished copy marker",
|
||||
description: "Somewhere on the page, text still carries a TODO marker.",
|
||||
skill_section: None,
|
||||
|
||||
@@ -43,6 +43,10 @@ Advisory findings:
|
||||
counted as failures and never changing the exit code. They stay out of the
|
||||
failure count so they never block automation. --no-advisory hides them.
|
||||
|
||||
Output streams:
|
||||
Human-readable findings go to stderr so stdout stays available for structured
|
||||
output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.
|
||||
|
||||
Project config:
|
||||
Respects .impeccable/config.json and .impeccable/config.local.json detector
|
||||
settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
|
||||
@@ -60,7 +64,7 @@ Detection modes:
|
||||
HTML files Static HTML/CSS analysis (default, catches linked CSS)
|
||||
Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)
|
||||
URLs Puppeteer full browser rendering (auto-detected;
|
||||
http(s):// and file:// URLs)
|
||||
http(s):// and file:// URLs; accessible linked CSS included)
|
||||
|
||||
Examples:
|
||||
impeccable detect src/
|
||||
@@ -77,8 +81,35 @@ fn format_finding_summary(count: usize) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// JS: main.mjs#expandJoinedUrlTargets
|
||||
///
|
||||
/// Some agent runners hand a shell-ready URL list to the process as one argv
|
||||
/// value. A browser accepts the spaces as part of one encoded URL, producing a
|
||||
/// plausible scan attributed to a bogus joined path. Expand only when every
|
||||
/// whitespace-delimited token is independently a URL, preserving ordinary
|
||||
/// filesystem paths that contain spaces.
|
||||
fn expand_joined_url_targets(targets: Vec<String>) -> Vec<String> {
|
||||
let mut out = Vec::with_capacity(targets.len());
|
||||
for target in targets {
|
||||
if !WHITESPACE_RE.is_match(&target) {
|
||||
out.push(target);
|
||||
continue;
|
||||
}
|
||||
let parts: Vec<&str> = WS_RUN_RE
|
||||
.split(impeccable_core::js::trim(&target))
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect();
|
||||
if parts.len() > 1 && parts.iter().all(|p| URL_RE.is_match(p)) {
|
||||
out.extend(parts.into_iter().map(str::to_string));
|
||||
} else {
|
||||
out.push(target);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn is_advisory(f: &Finding) -> bool {
|
||||
f.advisory == Some(true)
|
||||
f.advisory == Some(true) || f.severity == "advisory"
|
||||
}
|
||||
|
||||
fn partition_advisory(findings: &[Finding]) -> (Vec<&Finding>, Vec<&Finding>) {
|
||||
@@ -280,6 +311,8 @@ impl<'a> Ctx<'a> {
|
||||
|
||||
re!(VIEWPORT_RE, format!("^({D}{{2,5}})[xX]({D}{{2,5}})$"));
|
||||
re!(URL_RE, "^(?i:https?|file)://");
|
||||
re!(WHITESPACE_RE, impeccable_core::js::WS.to_string());
|
||||
re!(WS_RUN_RE, format!("{}+", impeccable_core::js::WS));
|
||||
re!(FILE_URL_RE, "^(?i:file):");
|
||||
|
||||
/// `fileURLToPath` for the `file:` URLs the CLI accepts; None when it can't map.
|
||||
@@ -455,11 +488,12 @@ fn detect_cli(args_in: &[String], io: &mut Io, engines: &Engines) -> Result<i32,
|
||||
// does sets this before handing the options to an engine.
|
||||
rule_pack: None,
|
||||
};
|
||||
let targets: Vec<String> = args
|
||||
.iter()
|
||||
.filter(|a| !a.starts_with("--"))
|
||||
.cloned()
|
||||
.collect();
|
||||
let targets: Vec<String> = expand_joined_url_targets(
|
||||
args.iter()
|
||||
.filter(|a| !a.starts_with("--"))
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
|
||||
if help_mode {
|
||||
io.out(USAGE);
|
||||
|
||||
@@ -15,7 +15,6 @@ static ROWS: &[Antipattern] = &[Antipattern {
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("warning"),
|
||||
advisory: false,
|
||||
name: "Unfinished copy marker",
|
||||
description: "Text still carries a TODO marker from drafting.",
|
||||
skill_section: None,
|
||||
|
||||
@@ -101,6 +101,12 @@ pub trait Dom {
|
||||
/// `document.documentElement.cloneNode(true)` with every
|
||||
/// `[id^="impeccable-live-"]` node removed, serialized as `outerHTML`.
|
||||
fn document_html_for_patterns(&self) -> String;
|
||||
/// The CSS of every readable linked stylesheet whose rules resolve to a
|
||||
/// live element, flattened out of its grouping rules (#709). Empty when
|
||||
/// the probe cannot read the CSSOM.
|
||||
fn linked_stylesheet_text(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
// ── element identity / tree ───────────────────────────────────────
|
||||
/// `el.tagName` (uppercase for HTML elements, as-is for SVG/foreign).
|
||||
|
||||
@@ -347,6 +347,10 @@ pub struct Snapshot {
|
||||
/// `[name, frames]` in stylesheet order (first rule per name wins).
|
||||
#[serde(default)]
|
||||
pub keyframes: Vec<(String, Vec<Vec<(String, String)>>)>,
|
||||
/// `__snapLinkedStylesheetText()`: the readable linked-stylesheet corpus
|
||||
/// (#709). Absent in captures older than that change.
|
||||
#[serde(rename = "linkedCss", default)]
|
||||
pub linked_css: String,
|
||||
/// The property columns of `SnapNode::style` (normally `STYLE_PROPS`;
|
||||
/// carried so an older capture stays readable).
|
||||
#[serde(rename = "styleProps", default)]
|
||||
@@ -691,6 +695,9 @@ impl Dom for SnapshotDom {
|
||||
fn document_html_for_patterns(&self) -> String {
|
||||
self.snap.html.clone()
|
||||
}
|
||||
fn linked_stylesheet_text(&self) -> String {
|
||||
self.snap.linked_css.clone()
|
||||
}
|
||||
fn tag_name(&self, el: ElId) -> String {
|
||||
self.snap.node(el).tag.clone()
|
||||
}
|
||||
|
||||
@@ -93,18 +93,21 @@ pub fn enclosing_css_selector(css_text: &str, index: usize) -> Option<String> {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let from = open.saturating_sub(1);
|
||||
// Ignore delimiters inside comments when locating the previous
|
||||
// declaration. Blanking each comment to its own length keeps every index
|
||||
// into the original source valid (#709).
|
||||
let before_open = SELECTOR_COMMENT_RE.replace_all(&css_text[..open], |c: ®ex::Captures| {
|
||||
" ".repeat(c[0].len())
|
||||
});
|
||||
let prev_close = match (
|
||||
last_index_of_byte(css_text, b'}', from),
|
||||
last_index_of_byte(css_text, b';', from),
|
||||
before_open.rfind('}'),
|
||||
before_open.rfind(';'),
|
||||
) {
|
||||
(Some(a), Some(b)) => Some(a.max(b)),
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(None, None) => None,
|
||||
};
|
||||
// JS: `lastIndexOf('}', open - 1)` with open == 0 clamps to index 0.
|
||||
let prev_close = if open == 0 { None } else { prev_close };
|
||||
let slice_start = prev_close.map(|p| p + 1).unwrap_or(0);
|
||||
let no_comments = SELECTOR_COMMENT_RE.replace_all(&css_text[slice_start..open], "");
|
||||
let raw_trim = js::trim(&no_comments);
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct Finding {
|
||||
#[serde(with = "crate::js::json_number")]
|
||||
pub line: f64,
|
||||
pub snippet: String,
|
||||
/// JS `advisory: true`, stamped only for advisory rules.
|
||||
/// JS `advisory: true`, derived from the effective severity (#709).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub advisory: Option<bool>,
|
||||
/// Extra keys spread onto the finding by callers, in insertion order.
|
||||
@@ -41,7 +41,7 @@ impl IgnorableFinding for Finding {
|
||||
/// JS `finding(id, filePath, snippet, line = 0)` for a rule already resolved
|
||||
/// from the registry.
|
||||
pub fn finding_for(ap: &Antipattern, file_path: &str, snippet: &str, line: f64) -> Finding {
|
||||
Finding {
|
||||
let mut f = Finding {
|
||||
antipattern: ap.id.to_string(),
|
||||
name: ap.name.to_string(),
|
||||
description: ap.description.to_string(),
|
||||
@@ -50,9 +50,22 @@ pub fn finding_for(ap: &Antipattern, file_path: &str, snippet: &str, line: f64)
|
||||
file: file_path.to_string(),
|
||||
line,
|
||||
snippet: snippet.to_string(),
|
||||
advisory: if ap.advisory { Some(true) } else { None },
|
||||
advisory: None,
|
||||
extras: Map::new(),
|
||||
}
|
||||
};
|
||||
derive_advisory_flag(&mut f);
|
||||
f
|
||||
}
|
||||
|
||||
/// JS: findings.mjs#deriveAdvisoryFlag. `advisory: true` is stamped when and
|
||||
/// only when the effective severity is `'advisory'`, so a per-finding severity
|
||||
/// promotion or demotion carries the flag with it (#709).
|
||||
pub fn derive_advisory_flag(item: &mut Finding) {
|
||||
item.advisory = if item.severity == "advisory" {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
/// JS `finding(id, filePath, snippet, line = 0)`. Returns `None` for an id
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
/// One `ANTIPATTERNS` entry. Optional fields are `None` where the JS object
|
||||
/// has no such key; `advisory` is `true` only where the JS has
|
||||
/// `advisory: true`.
|
||||
/// has no such key.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Antipattern {
|
||||
pub id: &'static str,
|
||||
@@ -21,8 +20,6 @@ pub struct Antipattern {
|
||||
/// JS `severity` (`'error'`, `'advisory'`); `finding()` defaults it to
|
||||
/// `'warning'` when absent.
|
||||
pub severity: Option<&'static str>,
|
||||
/// JS `advisory: true`.
|
||||
pub advisory: bool,
|
||||
pub name: &'static str,
|
||||
pub description: &'static str,
|
||||
/// JS `skillSection`.
|
||||
@@ -38,7 +35,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Side-tab accent border",
|
||||
description: "Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.",
|
||||
skill_section: Some("Visual Details"),
|
||||
@@ -49,7 +45,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Border accent on rounded element",
|
||||
description: "Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius.",
|
||||
skill_section: Some("Visual Details"),
|
||||
@@ -60,7 +55,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Overused font",
|
||||
description: "Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -71,7 +65,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Flat type hierarchy",
|
||||
description: "Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -82,7 +75,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Gradient text",
|
||||
description: "Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -93,7 +85,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "AI color palette",
|
||||
description: "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -104,7 +95,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Cream / beige palette",
|
||||
description: "A warm cream or beige page background has become the default \"tasteful\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -115,7 +105,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Nested cards",
|
||||
description: "Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -126,7 +115,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Monotonous spacing",
|
||||
description: "The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -137,7 +125,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Bounce or elastic easing",
|
||||
description: "Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.",
|
||||
skill_section: Some("Motion"),
|
||||
@@ -148,7 +135,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Pulsing status dot",
|
||||
description: "Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer.",
|
||||
skill_section: Some("Motion"),
|
||||
@@ -159,7 +145,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Decorative blinking cursor",
|
||||
description: "A blinking text cursor animated into a hero or landing section simulates typing where no input exists. It borrows the dev-tool aesthetic as decoration. Real editable fields draw their own caret; anywhere else, let the composition hold attention without a fake prompt.",
|
||||
skill_section: Some("Motion"),
|
||||
@@ -170,7 +155,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Shape-assembled illustration",
|
||||
description: "A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.",
|
||||
skill_section: Some("Imagery"),
|
||||
@@ -181,7 +165,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Organic contour drawn as clip-path",
|
||||
description: "A clip-path polygon with many arbitrary vertices, or a curved clip-path path(), is CSS approximating a torn edge, blob, or silhouette. It reads as the cheap version of the effect and is usually a produced or photographic material replaced with code. Derive an alpha matte from the real image, or ship the shape as a cut-out raster; keep clip-path for geometry (cut corners, diagonals, hexagons).",
|
||||
skill_section: Some("Imagery"),
|
||||
@@ -192,7 +175,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Raster buried under a wash or opacity",
|
||||
description: "A background image under a near-opaque gradient wash, or a raster on an element at near-zero opacity, never reaches the screen: the page shows the wash, and the produced texture or photo ships as a compliance token. Let the material show (a tint under 0.9 alpha, a blend mode, an opacity you can see) or remove the file.",
|
||||
skill_section: Some("Imagery"),
|
||||
@@ -203,7 +185,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Glowing shadow accents",
|
||||
description: "Colored glow shadows — a zero-offset chromatic halo (box- or text-shadow) on any background, or any colored blurred shadow on a dark background — are the default \"cool\" look of AI-generated UIs. Use neutral elevation shadows and subtle, purposeful lighting instead.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -214,7 +195,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Radial-gradient background halo",
|
||||
description: "A chromatic radial-gradient wash — saturated at the center, fading to transparent — used as a decorative background glow on a dark page. Same tell as glowing shadows, drawn with a gradient instead of a shadow. Ground the surface with a solid or subtly shifted background instead.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -225,7 +205,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Decorative radial spotlight glow",
|
||||
description: "A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a \"spotlight.\" It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -236,7 +215,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Auto-scrolling marquee",
|
||||
description: "Continuously auto-scrolling content demands attention it has not earned and hides half its content at any moment. Reserve motion for content that changes; let readers move at their own pace.",
|
||||
skill_section: Some("Motion"),
|
||||
@@ -247,7 +225,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Icon tile stacked above heading",
|
||||
description: "A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -258,7 +235,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Italic serif display headline",
|
||||
description: "Oversized italic serif (Fraunces, Recoleta, Playfair, Newsreader-italic) as the primary hero headline reads as taste in isolation but has become the universal AI-startup landing page hero. Set roman, or move to a non-serif display face. Editorial / magazine register may legitimately want this — judge by context.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -269,7 +245,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Hero eyebrow / pill chip",
|
||||
description: "A tiny uppercase letter-spaced label sitting immediately above an oversized hero headline — or the same shape rendered as a pill chip — is now the default AI SaaS hero. Drop the eyebrow, integrate the kicker into the headline, or run it as a navigation breadcrumb instead.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -280,7 +255,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Kicker / eyebrow label above heading",
|
||||
description: "A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -291,7 +265,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Tiny numbered section labels",
|
||||
description: "Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -301,8 +274,7 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
id: "em-dash-overuse",
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: true,
|
||||
severity: Some("advisory"),
|
||||
name: "Em-dash overuse",
|
||||
description: "Em-dash saturation in body copy is an AI cadence tell. Advisory only: humans use em-dashes legitimately, so this fires only on saturation — at least 8 em-dashes (— or --) at a density near one per 500 characters of body text — never on a long article that uses a few. Prefer commas, colons, periods, or parentheses.",
|
||||
skill_section: Some("Copy"),
|
||||
@@ -313,7 +285,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Marketing buzzword",
|
||||
description: "Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.",
|
||||
skill_section: Some("Copy"),
|
||||
@@ -324,7 +295,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Aphoristic-cadence copy",
|
||||
description: "Three or more sections landing on a short rebuttal sentence (\"X. No Y.\" / \"X. Just Y.\") or a manufactured-contrast aphorism (\"Not a feature. A platform.\") reads as AI cadence, not voice. Once is fine; the pattern is the tell.",
|
||||
skill_section: Some("Copy"),
|
||||
@@ -335,7 +305,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Oversized hero headline",
|
||||
description: "A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -346,7 +315,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Crushed letter spacing",
|
||||
description: "Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -357,7 +325,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Broken or placeholder image",
|
||||
description: "<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.",
|
||||
skill_section: Some("Imagery"),
|
||||
@@ -368,7 +335,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("error"),
|
||||
advisory: false,
|
||||
name: "Uncaught script error on load",
|
||||
description: "A script threw an uncaught exception or failed to parse while the page loaded. Broken JavaScript silently kills reveals, interactions, and dynamic content, and can leave most of a page invisible. Fix the error before judging anything else.",
|
||||
skill_section: None,
|
||||
@@ -379,7 +345,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: Some("error"),
|
||||
advisory: false,
|
||||
name: "Content invisible at rest",
|
||||
description: "A large share of the page text sits at opacity 0 or visibility hidden even after every reveal handler had a chance to run. This is the failed-reveal signature: the content shipped but never becomes visible. Make content visible by default and let JavaScript enhance its entrance instead of gating its existence.",
|
||||
skill_section: None,
|
||||
@@ -390,7 +355,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Cards flush against the scroller edge",
|
||||
description: "Cards inside a horizontal scroller or tab panel sit flush against the container edge at rest while keeping a gutter on the other side, so their edges and rounded corners get cut off. Usually the panel is sized wider than its clip box. Keep a consistent inset on both sides.",
|
||||
skill_section: None,
|
||||
@@ -401,7 +365,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Text occluded by an overlapping element",
|
||||
description: "Text is painted under an opaque element or a second text run, so part of it cannot be read. A decorative box, a stacked layer, or an inline element with leaked padding lands on the words instead of beside them. Give overlapping layers room, or move the text out from under the layer above it.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -412,7 +375,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "One column stretches the first viewport",
|
||||
description: "A multi-column opening section lets one column run far past the fold while its sibling fits in a single viewport, so the short column floats in dead space and the fold falls deep inside one section. Balance the columns, cap the tall one, or let the long content flow below the opening row.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -423,7 +385,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Gray text on colored background",
|
||||
description: "Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -434,7 +395,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Low contrast text",
|
||||
description: "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
skill_section: None,
|
||||
@@ -445,7 +405,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Layout property animation",
|
||||
description: "Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.",
|
||||
skill_section: Some("Motion"),
|
||||
@@ -456,7 +415,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type", "layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Line length too long",
|
||||
description: "Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -467,7 +425,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Cramped padding",
|
||||
description: "Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -478,7 +435,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Body text touching viewport edge",
|
||||
description: "Body paragraphs render flush against the left or right viewport edge with no container providing horizontal padding. Wrap content in a container with at least 16px (ideally 24-32px) of horizontal padding, or apply max-width with mx-auto.",
|
||||
skill_section: None,
|
||||
@@ -489,7 +445,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Tight line height",
|
||||
description: "Line height below 1.3x the font size makes multi-line text hard to read. Use 1.5 to 1.7 for body text so lines have room to breathe.",
|
||||
skill_section: None,
|
||||
@@ -500,7 +455,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Skipped heading level",
|
||||
description: "Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline.",
|
||||
skill_section: None,
|
||||
@@ -511,7 +465,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout", "type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Heading crowded against the previous block",
|
||||
description: "A heading binds to the content it introduces, so the rendered space above it should exceed the space below it. When headings across a page sit as close or closer to the block above than to their own content, every section reads as if it captions the previous one. Open up the space above each heading.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -522,7 +475,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Justified text",
|
||||
description: "Justified text without hyphenation creates uneven word spacing (\"rivers of white\"). Use text-align: left for body text, or enable hyphens: auto if you must justify.",
|
||||
skill_section: None,
|
||||
@@ -533,7 +485,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Tiny body text",
|
||||
description: "Body text below 12px is hard to read, especially on high-DPI screens. Use at least 14px for body content, 16px is ideal.",
|
||||
skill_section: None,
|
||||
@@ -544,7 +495,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Undersized functional text",
|
||||
description: "Interactive and content-bearing UI text (links, buttons, nav items, labels, table cells, meta rows, timecodes) below 11px is a legibility failure, not a style choice. WCAG sets no absolute pixel floor, but functional text under 11px is a defensible quality bar: it fails on high-DPI and small viewports and it degrades tap and read targets. The 11px floor holds even inside a footer; only non-interactive legal smallprint gets the softer 10px floor. Being ON the DESIGN.md size ramp does not exempt a value here: adding 8px to the ramp launders the token but not the legibility problem, and that is exactly the escape hatch this rule closes. Exempts sup/sub, visually-hidden (sr-only) text, and code/terminal contexts. Decorative letterspaced micro-labels are still functional and stay in scope.",
|
||||
skill_section: None,
|
||||
@@ -555,7 +505,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "All-caps body text",
|
||||
description: "Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -566,7 +515,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Wide letter spacing on body text",
|
||||
description: "Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.",
|
||||
skill_section: None,
|
||||
@@ -577,7 +525,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Content overflowing its container",
|
||||
description: "Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -588,7 +535,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Same text repeated inside one container",
|
||||
description: "The same literal text rendered three or more times in structurally different spots inside a single card or panel is redundant messaging — usually a status or label wired into every slot of a template. Say it once, in the slot where it matters most.",
|
||||
skill_section: None,
|
||||
@@ -599,7 +545,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["layout"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Positioned child clipped by overflow container",
|
||||
description: "A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.",
|
||||
skill_section: Some("Layout & Space"),
|
||||
@@ -610,7 +555,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Font outside DESIGN.md",
|
||||
description: "A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -621,7 +565,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Color outside DESIGN.md",
|
||||
description: "A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.",
|
||||
skill_section: Some("Color & Contrast"),
|
||||
@@ -632,7 +575,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Radius outside DESIGN.md",
|
||||
description: "A border-radius value is outside the DESIGN.md rounded scale. Use a documented radius token or update the design system if the new shape is intentional.",
|
||||
skill_section: Some("Visual Details"),
|
||||
@@ -643,7 +585,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "quality",
|
||||
scopes: Some(&["type"]),
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Font size outside DESIGN.md",
|
||||
description: "A literal font-size is off the type ramp documented in DESIGN.md typography. Use a documented size step or update the design system if the new step is intentional.",
|
||||
skill_section: Some("Typography"),
|
||||
@@ -654,7 +595,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Hairline border with wide shadow",
|
||||
description: "A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.",
|
||||
skill_section: Some("Visual Details"),
|
||||
@@ -665,7 +605,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Repeating-gradient stripes",
|
||||
description: "Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.",
|
||||
skill_section: Some("Visual Details"),
|
||||
@@ -676,7 +615,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Decorative grid-line background",
|
||||
description: "A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.",
|
||||
skill_section: Some("Visual Details"),
|
||||
@@ -687,7 +625,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Theater framing copy",
|
||||
description: "Dismissing something as \"theater\" is a recurring generated-copy tic. Say plainly what the thing does or does not do.",
|
||||
skill_section: Some("Copy"),
|
||||
@@ -698,7 +635,6 @@ pub static ANTIPATTERNS: &[Antipattern] = &[
|
||||
category: "slop",
|
||||
scopes: None,
|
||||
severity: Some("advisory"),
|
||||
advisory: false,
|
||||
name: "Image hover transform",
|
||||
description: "Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.",
|
||||
skill_section: Some("Motion"),
|
||||
@@ -834,10 +770,12 @@ pub fn get_ap(id: &str) -> Option<&'static Antipattern> {
|
||||
get_antipattern(id)
|
||||
}
|
||||
|
||||
/// JS `ADVISORY_RULE_IDS`: ids of rules with `advisory: true`, in registry order.
|
||||
/// JS `ADVISORY_RULE_IDS`: ids of rules whose registry severity is
|
||||
/// `'advisory'`, in registry order. `severity` is the canonical field; the
|
||||
/// finding serializer derives its `advisory: true` output flag from it (#709).
|
||||
pub fn advisory_rule_ids() -> impl Iterator<Item = &'static str> {
|
||||
all_antipatterns()
|
||||
.filter(|rule| rule.advisory)
|
||||
.filter(|rule| rule.severity == Some("advisory"))
|
||||
.map(|rule| rule.id)
|
||||
}
|
||||
|
||||
@@ -906,7 +844,9 @@ mod tests {
|
||||
assert_eq!(ANTIPATTERNS[0].id, "side-tab");
|
||||
assert_eq!(rule_scopes(), vec!["type", "layout"]);
|
||||
assert!(is_advisory_rule("em-dash-overuse"));
|
||||
assert!(!is_advisory_rule("blinking-cursor"));
|
||||
// #709: severity is the canonical advisory field, so every
|
||||
// `severity: "advisory"` rule is an advisory rule.
|
||||
assert!(is_advisory_rule("blinking-cursor"));
|
||||
assert_eq!(
|
||||
get_antipattern("blinking-cursor").unwrap().severity,
|
||||
Some("advisory")
|
||||
@@ -929,8 +869,7 @@ mod tests {
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("warning"),
|
||||
advisory: false,
|
||||
name: "Test pack rule one",
|
||||
name: "Test pack rule one",
|
||||
description: "First row of the registry-extension test pack.",
|
||||
skill_section: None,
|
||||
skill_guideline: None,
|
||||
@@ -940,8 +879,7 @@ mod tests {
|
||||
category: "testpack-only",
|
||||
scopes: None,
|
||||
severity: Some("error"),
|
||||
advisory: false,
|
||||
name: "Test pack rule two",
|
||||
name: "Test pack rule two",
|
||||
description: "Second row of the registry-extension test pack.",
|
||||
skill_section: None,
|
||||
skill_guideline: None,
|
||||
@@ -953,7 +891,6 @@ mod tests {
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: None,
|
||||
advisory: false,
|
||||
name: "Collides with a built-in",
|
||||
description: "Registering this must panic.",
|
||||
skill_section: None,
|
||||
|
||||
@@ -104,12 +104,18 @@ pub fn depth_is_set(value: Option<&str>) -> bool {
|
||||
/// same list without linking this native-only crate.
|
||||
pub use impeccable_core::registry::IMMEDIATE_TIER_RULES;
|
||||
|
||||
/// A legacy id fallback that keeps older detector findings recognizable when
|
||||
/// they carry neither the runtime flag nor the canonical advisory severity.
|
||||
/// Current findings are classified by their serialized metadata (#709).
|
||||
pub const ADVISORY_RULES: &[&str] = &["em-dash-overuse"];
|
||||
|
||||
/// JS: isAdvisoryFinding(finding)
|
||||
pub fn is_advisory_finding(f: &Finding) -> bool {
|
||||
let id = normalize_ignore_rule(&f.antipattern);
|
||||
!id.is_empty() && (ADVISORY_RULES.contains(&id.as_str()) || f.advisory == Some(true))
|
||||
!id.is_empty()
|
||||
&& (ADVISORY_RULES.contains(&id.as_str())
|
||||
|| f.advisory == Some(true)
|
||||
|| f.severity == "advisory")
|
||||
}
|
||||
|
||||
pub const HOOK_LOCAL_IGNORE_PATTERNS: &[&str] = &[
|
||||
|
||||
@@ -335,6 +335,7 @@ pub fn detect_html_source(
|
||||
if let Some(sev) = f.severity.as_ref() {
|
||||
item.severity = sev.clone();
|
||||
}
|
||||
impeccable_core::findings::derive_advisory_flag(&mut item);
|
||||
findings.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ static ROWS: &[Antipattern] = &[Antipattern {
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("warning"),
|
||||
advisory: false,
|
||||
name: "Unfinished copy marker",
|
||||
description: "Text still carries a TODO marker from drafting.",
|
||||
skill_section: None,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -253,9 +253,6 @@ fn antipattern_json(ap: &impeccable_core::registry::Antipattern) -> Value {
|
||||
if let Some(s) = ap.severity {
|
||||
m.insert("severity".into(), json!(s));
|
||||
}
|
||||
if ap.advisory {
|
||||
m.insert("advisory".into(), json!(true));
|
||||
}
|
||||
m.insert("name".into(), json!(ap.name));
|
||||
m.insert("description".into(), json!(ap.description));
|
||||
if let Some(s) = ap.skill_section {
|
||||
|
||||
@@ -28,6 +28,7 @@ extern "C" {
|
||||
fn css_escape(s: &str) -> String;
|
||||
fn keyframes(name: &str) -> Option<String>;
|
||||
fn document_html_for_patterns() -> String;
|
||||
fn linked_stylesheet_text() -> String;
|
||||
fn tag_name(el: u32) -> String;
|
||||
fn namespace_uri(el: u32) -> String;
|
||||
fn parent(el: u32) -> u32;
|
||||
@@ -177,6 +178,9 @@ impl Dom for JsDom {
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
fn linked_stylesheet_text(&self) -> String {
|
||||
linked_stylesheet_text()
|
||||
}
|
||||
fn document_html_for_patterns(&self) -> String {
|
||||
document_html_for_patterns()
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ pub fn antipatterns_json() -> String {
|
||||
"name": ap.name,
|
||||
"category": ap.category,
|
||||
"severity": ap.severity,
|
||||
"advisory": ap.advisory,
|
||||
"advisory": ap.severity == Some("advisory"),
|
||||
"description": ap.description,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ static ROWS: &[Antipattern] = &[Antipattern {
|
||||
category: "quality",
|
||||
scopes: None,
|
||||
severity: Some("warning"),
|
||||
advisory: false,
|
||||
name: "Unfinished copy marker",
|
||||
description: "Text still carries a TODO marker from drafting.",
|
||||
skill_section: None,
|
||||
|
||||
@@ -130,6 +130,10 @@ Advisory findings:
|
||||
counted as failures and never changing the exit code. They stay out of the
|
||||
failure count so they never block automation. --no-advisory hides them.
|
||||
|
||||
Output streams:
|
||||
Human-readable findings go to stderr so stdout stays available for structured
|
||||
output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.
|
||||
|
||||
Project config:
|
||||
Respects .impeccable/config.json and .impeccable/config.local.json detector
|
||||
settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
|
||||
@@ -147,7 +151,7 @@ Detection modes:
|
||||
HTML files Static HTML/CSS analysis (default, catches linked CSS)
|
||||
Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)
|
||||
URLs Puppeteer full browser rendering (auto-detected;
|
||||
http(s):// and file:// URLs)
|
||||
http(s):// and file:// URLs; accessible linked CSS included)
|
||||
|
||||
Examples:
|
||||
impeccable detect src/
|
||||
@@ -222,9 +226,9 @@ Example (non-TTY):
|
||||
**Finding object** (`cli/engine/findings.mjs` `finding(id, filePath, snippet, line = 0)`), key order exactly:
|
||||
```js
|
||||
{ antipattern: id, name: ap.name, description: ap.description, severity: ap.severity || 'warning', category: ap.category || null, file: filePath, line, snippet }
|
||||
// plus, only when registry rule has `advisory: true`: advisory: true
|
||||
// plus, only when the effective severity is 'advisory': advisory: true
|
||||
```
|
||||
Optional keys added later by engines (appended after the above): `ignoreValue` (design-system rules; browser findings with a value), `importedBy` (dir scans), `severity` may be overwritten by per-finding promotion (browser & html-patterns, e.g. pulsing dot in a header). Design-system findings are `{...finding(...), ...extras}` where extras = `{ ignoreValue }`. Static-HTML and browser findings have `line: 0`; regex findings have 1-based lines. `severity` values in registry: `'warning'` (default), `'advisory'` (many generated-UI tells and design-system-color/radius/font-size, numbered-section-labels, blinking-cursor, shape-assembled-illustration), `'error'` (`script-error`, `content-hidden-at-rest`). **Only `em-dash-overuse` has `advisory: true`**; `severity:'advisory'` alone does NOT make a finding advisory for exit-code/partition purposes (isAdvisory checks `finding.advisory === true`).
|
||||
Optional keys added later by engines (appended after the above): `ignoreValue` (design-system rules; browser findings with a value), `importedBy` (dir scans), `severity` may be overwritten by per-finding promotion (browser & html-patterns, e.g. pulsing dot in a header). Design-system findings are `{...finding(...), ...extras}` where extras = `{ ignoreValue }`. Static-HTML and browser findings have `line: 0`; regex findings have 1-based lines. `severity` values in registry: `'warning'` (default), `'advisory'` (many generated-UI tells and design-system-color/radius/font-size, numbered-section-labels, blinking-cursor, shape-assembled-illustration), `'error'` (`script-error`, `content-hidden-at-rest`). `severity` is the canonical advisory field (#709): `deriveAdvisoryFlag` stamps `advisory: true` when and only when the effective severity is `'advisory'`, so a per-finding promotion or demotion carries the flag with it, and every `severity:'advisory'` rule is partitioned out of the failure count and the exit code. `isAdvisory` accepts either `finding.advisory === true` or `finding.severity === 'advisory'`.
|
||||
|
||||
**Categories**: `category` is `'slop'` (AI tells) or `'quality'`. Category has **no effect on output**, ordering, or exit codes; it is only carried in the finding and used by `getRulesForCategory`. Registry (59 ids, in order): side-tab, border-accent-on-rounded, overused-font, flat-type-hierarchy, gradient-text, ai-color-palette, cream-palette, nested-cards, monotonous-spacing, bounce-easing, pulsing-dot, blinking-cursor, shape-assembled-illustration, dark-glow, radial-halo, radial-spotlight-glow, marquee, icon-tile-stack, italic-serif-display, hero-eyebrow-chip, kicker-above-heading, numbered-section-labels, em-dash-overuse, marketing-buzzword, aphoristic-cadence, oversized-h1, extreme-negative-tracking, broken-image, script-error, content-hidden-at-rest, edge-flush-cards, text-occlusion, first-viewport-column-overflow, gray-on-color, low-contrast, layout-transition, line-length, cramped-padding, body-text-viewport-edge, tight-leading, skipped-heading, heading-rhythm, justified-text, tiny-text, undersized-ui-text, all-caps-body, wide-tracking, text-overflow, repeated-container-text, clipped-overflow-container, design-system-font, design-system-color, design-system-radius, design-system-font-size, gpt-thin-border-wide-shadow, repeating-stripes-gradient, codex-grid-background, theater-slop-phrase, image-hover-transform. Scopes: `type` = overused-font, flat-type-hierarchy, italic-serif-display, hero-eyebrow-chip, kicker-above-heading, numbered-section-labels, oversized-h1, extreme-negative-tracking, line-length, tight-leading, skipped-heading, heading-rhythm, justified-text, tiny-text, undersized-ui-text, all-caps-body, wide-tracking, design-system-font, design-system-font-size; `layout` = nested-cards, monotonous-spacing, icon-tile-stack, content-hidden-at-rest, edge-flush-cards, text-occlusion, first-viewport-column-overflow, line-length, cramped-padding, body-text-viewport-edge, heading-rhythm, text-overflow, clipped-overflow-container. `RULE_ENGINE_SUPPORT = { regex: Set['source','page-analyzer'], 'static-html': Set['element','page'], browser: Set['element','page','layout'], visual: Set['visual-contrast'] }`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/blinking-cursor.html\",\n \"line\": 20,\n \"snippet\": \"Undocumented color #33d17a is outside DESIGN.md colors\",\n \"ignoreValue\": \"#33d17a\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/blinking-cursor.html\",\n \"line\": 58,\n \"snippet\": \"Undocumented color #999 is outside DESIGN.md colors\",\n \"ignoreValue\": \"#999\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/blinking-cursor.html\",\n \"line\": 20,\n \"snippet\": \"Undocumented color #33d17a is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"#33d17a\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/blinking-cursor.html\",\n \"line\": 58,\n \"snippet\": \"Undocumented color #999 is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"#999\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 1,\n \"snippet\": \"cubic-bezier(0.68, -0.55, 0.265, 1.55)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 2,\n \"snippet\": \"Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgba(99, 102, 241, 0.6)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 1,\n \"snippet\": \"cubic-bezier(0.68, -0.55, 0.265, 1.55)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 2,\n \"snippet\": \"Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgba(99, 102, 241, 0.6)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<WS>/src/styles.css\n line 1: [bounce-easing] cubic-bezier(0.68, -0.55, 0.265, 1.55)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 2: [design-system-color] Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n2 anti-patterns found.\n",
|
||||
"stderr": "\n<WS>/src/styles.css\n line 1: [bounce-easing] cubic-bezier(0.68, -0.55, 0.265, 1.55)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n1 anti-pattern found.\n\n── Advisory (not counted as failures) ──\n\n<WS>/src/styles.css\n line 2: [design-system-color] Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n1 advisory note. Suppress with --no-advisory.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 1,\n \"snippet\": \"cubic-bezier(0.68, -0.55, 0.265, 1.55)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 2,\n \"snippet\": \"Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgba(99, 102, 241, 0.6)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 1,\n \"snippet\": \"cubic-bezier(0.68, -0.55, 0.265, 1.55)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 2,\n \"snippet\": \"Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgba(99, 102, 241, 0.6)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 1,\n \"snippet\": \"cubic-bezier(0.68, -0.55, 0.265, 1.55)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 2,\n \"snippet\": \"Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgba(99, 102, 241, 0.6)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"bounce-easing\",\n \"name\": \"Bounce or elastic easing\",\n \"description\": \"Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 1,\n \"snippet\": \"cubic-bezier(0.68, -0.55, 0.265, 1.55)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/styles.css\",\n \"line\": 2,\n \"snippet\": \"Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgba(99, 102, 241, 0.6)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<WS>/src/inline.html\n line 5: [design-system-color] text color rgb(255, 0, 170) on p \"faint text\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n [design-system-color] text color rgb(0, 0, 0) on p \"x\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n<WS>/src/page.html\n [low-contrast] 3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n line 3: [design-system-font] div \"Hi\" uses verdana; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n [design-system-color] text color rgb(0, 0, 0) on div \"Hi\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n line 4: [design-system-font] p \"Brand\" uses inter; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n line 6: [design-system-color] text color rgb(255, 0, 170) on p \"Off palette\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n<WS>/src/styles.css\n line 1: [bounce-easing] cubic-bezier(0.68, -0.55, 0.265, 1.55)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n line 2: [design-system-color] Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n9 anti-patterns found.\n",
|
||||
"stderr": "\n<WS>/src/page.html\n [low-contrast] 3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n line 3: [design-system-font] div \"Hi\" uses verdana; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n line 4: [design-system-font] p \"Brand\" uses inter; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n\n<WS>/src/styles.css\n line 1: [bounce-easing] cubic-bezier(0.68, -0.55, 0.265, 1.55)\n → Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.\n\n4 anti-patterns found.\n\n── Advisory (not counted as failures) ──\n\n<WS>/src/inline.html\n line 5: [design-system-color] text color rgb(255, 0, 170) on p \"faint text\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n [design-system-color] text color rgb(0, 0, 0) on p \"x\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n<WS>/src/page.html\n [design-system-color] text color rgb(0, 0, 0) on div \"Hi\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n line 6: [design-system-color] text color rgb(255, 0, 170) on p \"Off palette\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n<WS>/src/styles.css\n line 2: [design-system-color] Undocumented color rgba(99, 102, 241, 0.6) is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n5 advisory notes. Suppress with --no-advisory.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"overused-font\",\n \"name\": \"Overused font\",\n \"description\": \"Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"Primary font: inter\"\n },\n {\n \"antipattern\": \"pulsing-dot\",\n \"name\": \"Pulsing status dot\",\n \"description\": \"Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \".dot — 8x8px dot with infinite \\\"blink\\\" animation\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"overused-font\",\n \"name\": \"Overused font\",\n \"description\": \"Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"Primary font: inter\"\n },\n {\n \"antipattern\": \"pulsing-dot\",\n \"name\": \"Pulsing status dot\",\n \"description\": \"Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \".dot — 8x8px dot with infinite \\\"blink\\\" animation\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 6,\n \"snippet\": \"p \\\"x\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 6,\n \"snippet\": \"p \\\"x\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 5,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"faint text\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/inline.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on p \\\"x\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"low-contrast\",\n \"name\": \"Low contrast text\",\n \"description\": \"Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 3,\n \"snippet\": \"div \\\"Hi\\\" uses verdana; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"verdana\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 0,\n \"snippet\": \"text color rgb(0, 0, 0) on div \\\"Hi\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(0, 0, 0)\"\n },\n {\n \"antipattern\": \"design-system-font\",\n \"name\": \"Font outside DESIGN.md\",\n \"description\": \"A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\",\n \"severity\": \"warning\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 4,\n \"snippet\": \"p \\\"Brand\\\" uses inter; not declared in DESIGN.md typography\",\n \"ignoreValue\": \"inter\"\n },\n {\n \"antipattern\": \"design-system-color\",\n \"name\": \"Color outside DESIGN.md\",\n \"description\": \"A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\",\n \"severity\": \"advisory\",\n \"category\": \"quality\",\n \"file\": \"<WS>/src/page.html\",\n \"line\": 6,\n \"snippet\": \"text color rgb(255, 0, 170) on p \\\"Off palette\\\" is outside DESIGN.md colors\",\n \"advisory\": true,\n \"ignoreValue\": \"rgb(255, 0, 170)\"\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<WS>/src/page.html\n [low-contrast] 3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n line 3: [design-system-font] div \"Hi\" uses verdana; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n [design-system-color] text color rgb(0, 0, 0) on div \"Hi\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n line 4: [design-system-font] p \"Brand\" uses inter; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n line 6: [design-system-color] text color rgb(255, 0, 170) on p \"Off palette\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n5 anti-patterns found.\n",
|
||||
"stderr": "\n<WS>/src/page.html\n [low-contrast] 3.6:1 (need 4.5:1) — text #ff00aa on #ffffff\n → Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.\n line 3: [design-system-font] div \"Hi\" uses verdana; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n line 4: [design-system-font] p \"Brand\" uses inter; not declared in DESIGN.md typography\n → A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.\n\n3 anti-patterns found.\n\n── Advisory (not counted as failures) ──\n\n<WS>/src/page.html\n [design-system-color] text color rgb(0, 0, 0) on div \"Hi\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n line 6: [design-system-color] text color rgb(255, 0, 170) on p \"Off palette\" is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n2 advisory notes. Suppress with --no-advisory.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"em-dash-overuse\",\n \"name\": \"Em-dash overuse\",\n \"description\": \"Em-dash saturation in body copy is an AI cadence tell. Advisory only: humans use em-dashes legitimately, so this fires only on saturation — at least 8 em-dashes (— or --) at a density near one per 500 characters of body text — never on a long article that uses a few. Prefer commas, colons, periods, or parentheses.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/em-dash-entities.html\",\n \"line\": 0,\n \"snippet\": \"8 em-dashes in body text\",\n \"advisory\": true\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"em-dash-overuse\",\n \"name\": \"Em-dash overuse\",\n \"description\": \"Em-dash saturation in body copy is an AI cadence tell. Advisory only: humans use em-dashes legitimately, so this fires only on saturation — at least 8 em-dashes (— or --) at a density near one per 500 characters of body text — never on a long article that uses a few. Prefer commas, colons, periods, or parentheses.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/em-dash-entities.html\",\n \"line\": 0,\n \"snippet\": \"8 em-dashes in body text\",\n \"advisory\": true\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"image-hover-transform\",\n \"name\": \"Image hover transform\",\n \"description\": \"Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gemini-tells.html\",\n \"line\": 0,\n \"snippet\": \"img:hover { transform } rule\"\n },\n {\n \"antipattern\": \"image-hover-transform\",\n \"name\": \"Image hover transform\",\n \"description\": \"Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gemini-tells.html\",\n \"line\": 0,\n \"snippet\": \"Tailwind hover transform on <img>\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"image-hover-transform\",\n \"name\": \"Image hover transform\",\n \"description\": \"Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gemini-tells.html\",\n \"line\": 0,\n \"snippet\": \"img:hover { transform } rule\",\n \"advisory\": true\n },\n {\n \"antipattern\": \"image-hover-transform\",\n \"name\": \"Image hover transform\",\n \"description\": \"Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gemini-tells.html\",\n \"line\": 0,\n \"snippet\": \"Tailwind hover transform on <img>\",\n \"advisory\": true\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"gpt-thin-border-wide-shadow\",\n \"name\": \"Hairline border with wide shadow\",\n \"description\": \"A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"1px border + 24px shadow blur\"\n },\n {\n \"antipattern\": \"repeating-stripes-gradient\",\n \"name\": \"Repeating-gradient stripes\",\n \"description\": \"Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"repeating-gradient decorative stripes\"\n },\n {\n \"antipattern\": \"codex-grid-background\",\n \"name\": \"Decorative grid-line background\",\n \"description\": \"A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"two-axis grid-line gradient background\"\n },\n {\n \"antipattern\": \"theater-slop-phrase\",\n \"name\": \"Theater framing copy\",\n \"description\": \"Dismissing something as \\\"theater\\\" is a recurring generated-copy tic. Say plainly what the thing does or does not do.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"\\\"growth theater\\\"\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"gpt-thin-border-wide-shadow\",\n \"name\": \"Hairline border with wide shadow\",\n \"description\": \"A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"1px border + 24px shadow blur\",\n \"advisory\": true\n },\n {\n \"antipattern\": \"repeating-stripes-gradient\",\n \"name\": \"Repeating-gradient stripes\",\n \"description\": \"Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"repeating-gradient decorative stripes\",\n \"advisory\": true\n },\n {\n \"antipattern\": \"codex-grid-background\",\n \"name\": \"Decorative grid-line background\",\n \"description\": \"A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"two-axis grid-line gradient background\",\n \"advisory\": true\n },\n {\n \"antipattern\": \"theater-slop-phrase\",\n \"name\": \"Theater framing copy\",\n \"description\": \"Dismissing something as \\\"theater\\\" is a recurring generated-copy tic. Say plainly what the thing does or does not do.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/gpt-tells.html\",\n \"line\": 0,\n \"snippet\": \"\\\"growth theater\\\"\",\n \"advisory\": true\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"01\\\" beside h2 \\\"Alpha ships first\\\" (4 on page)\"\n },\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"02\\\" beside h2 \\\"Beta earns trust\\\" (4 on page)\"\n },\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"03\\\" beside h2 \\\"Gamma holds the line\\\" (4 on page)\"\n },\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"04 / ROLLOUT\\\" beside h2 \\\"Delta closes the loop\\\" (4 on page)\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"01\\\" beside h2 \\\"Alpha ships first\\\" (4 on page)\",\n \"advisory\": true\n },\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"02\\\" beside h2 \\\"Beta earns trust\\\" (4 on page)\",\n \"advisory\": true\n },\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"03\\\" beside h2 \\\"Gamma holds the line\\\" (4 on page)\",\n \"advisory\": true\n },\n {\n \"antipattern\": \"numbered-section-labels\",\n \"name\": \"Tiny numbered section labels\",\n \"description\": \"Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\",\n \"line\": 0,\n \"snippet\": \"tiny numbered label \\\"04 / ROLLOUT\\\" beside h2 \\\"Delta closes the loop\\\" (4 on page)\",\n \"advisory\": true\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 6px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 8px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 12px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 5px\"\n },\n {\n \"antipattern\": \"overused-font\",\n \"name\": \"Overused font\",\n \"description\": \"Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"Primary font: arial\"\n },\n {\n \"antipattern\": \"cream-palette\",\n \"name\": \"Cream / beige palette\",\n \"description\": \"A warm cream or beige page background has become the default \\\"tasteful\\\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"cream/beige page background rgb(250, 248, 244)\"\n },\n {\n \"antipattern\": \"codex-grid-background\",\n \"name\": \"Decorative grid-line background\",\n \"description\": \"A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"two-axis grid-line gradient background\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 6px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 8px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 12px\"\n },\n {\n \"antipattern\": \"side-tab\",\n \"name\": \"Side-tab accent border\",\n \"description\": \"Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"border-left: 5px\"\n },\n {\n \"antipattern\": \"overused-font\",\n \"name\": \"Overused font\",\n \"description\": \"Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"Primary font: arial\"\n },\n {\n \"antipattern\": \"cream-palette\",\n \"name\": \"Cream / beige palette\",\n \"description\": \"A warm cream or beige page background has become the default \\\"tasteful\\\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.\",\n \"severity\": \"warning\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"cream/beige page background rgb(250, 248, 244)\"\n },\n {\n \"antipattern\": \"codex-grid-background\",\n \"name\": \"Decorative grid-line background\",\n \"description\": \"A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\",\n \"line\": 0,\n \"snippet\": \"two-axis grid-line gradient background\",\n \"advisory\": true\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "[\n {\n \"antipattern\": \"shape-assembled-illustration\",\n \"name\": \"Shape-assembled illustration\",\n \"description\": \"A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/shape-assembled-illustration.html\",\n \"line\": 0,\n \"snippet\": \"inline <svg> scene: 12 primitive shapes, ~640x480px, 6 fill colors\"\n }\n]\n",
|
||||
"stdout": "[\n {\n \"antipattern\": \"shape-assembled-illustration\",\n \"name\": \"Shape-assembled illustration\",\n \"description\": \"A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.\",\n \"severity\": \"advisory\",\n \"category\": \"slop\",\n \"file\": \"<REPO>/tests/fixtures/antipatterns/shape-assembled-illustration.html\",\n \"line\": 0,\n \"snippet\": \"inline <svg> scene: 12 primitive shapes, ~640x480px, 6 fill colors\",\n \"advisory\": true\n }\n]\n",
|
||||
"stderr": "",
|
||||
"exit": 2,
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/gemini-tells.html\n [image-hover-transform] img:hover { transform } rule\n → Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\n [image-hover-transform] Tailwind hover transform on <img>\n → Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\n\n2 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"stderr": "\n0 anti-patterns found.\n\n── Advisory (not counted as failures) ──\n\n<REPO>/tests/fixtures/antipatterns/gemini-tells.html\n [image-hover-transform] img:hover { transform } rule\n → Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\n [image-hover-transform] Tailwind hover transform on <img>\n → Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.\n\n2 advisory notes. Suppress with --no-advisory.\n",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/gpt-tells.html\n [gpt-thin-border-wide-shadow] 1px border + 24px shadow blur\n → A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.\n [repeating-stripes-gradient] repeating-gradient decorative stripes\n → Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.\n [codex-grid-background] two-axis grid-line gradient background\n → A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\n [theater-slop-phrase] \"growth theater\"\n → Dismissing something as \"theater\" is a recurring generated-copy tic. Say plainly what the thing does or does not do.\n\n4 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"stderr": "\n0 anti-patterns found.\n\n── Advisory (not counted as failures) ──\n\n<REPO>/tests/fixtures/antipatterns/gpt-tells.html\n [gpt-thin-border-wide-shadow] 1px border + 24px shadow blur\n → A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.\n [repeating-stripes-gradient] repeating-gradient decorative stripes\n → Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.\n [codex-grid-background] two-axis grid-line gradient background\n → A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\n [theater-slop-phrase] \"growth theater\"\n → Dismissing something as \"theater\" is a recurring generated-copy tic. Say plainly what the thing does or does not do.\n\n4 advisory notes. Suppress with --no-advisory.\n",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\n [numbered-section-labels] tiny numbered label \"01\" beside h2 \"Alpha ships first\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n [numbered-section-labels] tiny numbered label \"02\" beside h2 \"Beta earns trust\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n [numbered-section-labels] tiny numbered label \"03\" beside h2 \"Gamma holds the line\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n [numbered-section-labels] tiny numbered label \"04 / ROLLOUT\" beside h2 \"Delta closes the loop\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n\n4 anti-patterns found.\n",
|
||||
"exit": 2,
|
||||
"stderr": "\n0 anti-patterns found.\n\n── Advisory (not counted as failures) ──\n\n<REPO>/tests/fixtures/antipatterns/numbered-section-labels.html\n [numbered-section-labels] tiny numbered label \"01\" beside h2 \"Alpha ships first\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n [numbered-section-labels] tiny numbered label \"02\" beside h2 \"Beta earns trust\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n [numbered-section-labels] tiny numbered label \"03\" beside h2 \"Gamma holds the line\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n [numbered-section-labels] tiny numbered label \"04 / ROLLOUT\" beside h2 \"Delta closes the loop\" (4 on page)\n → Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.\n\n4 advisory notes. Suppress with --no-advisory.\n",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\n [side-tab] border-left: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 8px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 12px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 5px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [overused-font] Primary font: arial\n → Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\n [cream-palette] cream/beige page background rgb(250, 248, 244)\n → A warm cream or beige page background has become the default \"tasteful\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.\n [codex-grid-background] two-axis grid-line gradient background\n → A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\n\n7 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\n [side-tab] border-left: 6px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 8px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 12px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [side-tab] border-left: 5px\n → Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.\n [overused-font] Primary font: arial\n → Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.\n [cream-palette] cream/beige page background rgb(250, 248, 244)\n → A warm cream or beige page background has become the default \"tasteful\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.\n\n6 anti-patterns found.\n\n── Advisory (not counted as failures) ──\n\n<REPO>/tests/fixtures/antipatterns/scoped-ignore.html\n [codex-grid-background] two-axis grid-line gradient background\n → A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.\n\n1 advisory note. Suppress with --no-advisory.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/shape-assembled-illustration.html\n [shape-assembled-illustration] inline <svg> scene: 12 primitive shapes, ~640x480px, 6 fill colors\n → A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.\n\n1 anti-pattern found.\n",
|
||||
"exit": 2,
|
||||
"stderr": "\n0 anti-patterns found.\n\n── Advisory (not counted as failures) ──\n\n<REPO>/tests/fixtures/antipatterns/shape-assembled-illustration.html\n [shape-assembled-illustration] inline <svg> scene: 12 primitive shapes, ~640x480px, 6 fill colors\n → A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.\n\n1 advisory note. Suppress with --no-advisory.\n",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Usage: impeccable detect [options] [file-or-dir-or-url...]\n\nScan files or URLs for UI anti-patterns and design quality issues.\n\nOptions:\n --json Output results as JSON\n --quiet In text mode, only print the final findings count\n --scope <name> Only report rules in the given design domain\n (type, layout). Comma-separated.\n --viewport <WxH> Browser viewport for URL scans (default 1280x800),\n e.g. --viewport 390x844 for a mobile-width pass\n --no-config Do not apply project config, detector ignores, inline\n ignore comments, or DESIGN.md\n --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments\n --no-design-system Do not load local DESIGN.md / .impeccable/design.json context\n --no-advisory Suppress advisory findings entirely (e.g. em-dash overuse)\n --help Show this help message\n\nAdvisory findings:\n Some rules are advisory: detected and listed in a separate section, but never\n counted as failures and never changing the exit code. They stay out of the\n failure count so they never block automation. --no-advisory hides them.\n\nProject config:\n Respects .impeccable/config.json and .impeccable/config.local.json detector\n settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,\n and detector.designSystem.enabled.\n\nInline ignores:\n In-file comments waive a finding where it lives and travel with the file:\n <!-- impeccable-disable overused-font -- exported brand doc -->\n .brand { font-family: Inter } /* impeccable-disable-line overused-font */\n // impeccable-disable-next-line bounce-easing: intentional bounce\n impeccable-disable applies to the whole file; -line / -next-line are scoped.\n List one or more rule ids (comma-separated), or omit them / use * for all.\n\nDetection modes:\n HTML files Static HTML/CSS analysis (default, catches linked CSS)\n Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)\n URLs Puppeteer full browser rendering (auto-detected;\n http(s):// and file:// URLs)\n\nExamples:\n impeccable detect src/\n impeccable detect index.html\n impeccable detect https://example.com\n impeccable detect --json .\n impeccable detect --no-config src/\n",
|
||||
"stdout": "Usage: impeccable detect [options] [file-or-dir-or-url...]\n\nScan files or URLs for UI anti-patterns and design quality issues.\n\nOptions:\n --json Output results as JSON\n --quiet In text mode, only print the final findings count\n --scope <name> Only report rules in the given design domain\n (type, layout). Comma-separated.\n --viewport <WxH> Browser viewport for URL scans (default 1280x800),\n e.g. --viewport 390x844 for a mobile-width pass\n --no-config Do not apply project config, detector ignores, inline\n ignore comments, or DESIGN.md\n --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments\n --no-design-system Do not load local DESIGN.md / .impeccable/design.json context\n --no-advisory Suppress advisory findings entirely (e.g. em-dash overuse)\n --help Show this help message\n\nAdvisory findings:\n Some rules are advisory: detected and listed in a separate section, but never\n counted as failures and never changing the exit code. They stay out of the\n failure count so they never block automation. --no-advisory hides them.\n\nOutput streams:\n Human-readable findings go to stderr so stdout stays available for structured\n output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.\n\nProject config:\n Respects .impeccable/config.json and .impeccable/config.local.json detector\n settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,\n and detector.designSystem.enabled.\n\nInline ignores:\n In-file comments waive a finding where it lives and travel with the file:\n <!-- impeccable-disable overused-font -- exported brand doc -->\n .brand { font-family: Inter } /* impeccable-disable-line overused-font */\n // impeccable-disable-next-line bounce-easing: intentional bounce\n impeccable-disable applies to the whole file; -line / -next-line are scoped.\n List one or more rule ids (comma-separated), or omit them / use * for all.\n\nDetection modes:\n HTML files Static HTML/CSS analysis (default, catches linked CSS)\n Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)\n URLs Puppeteer full browser rendering (auto-detected;\n http(s):// and file:// URLs; accessible linked CSS included)\n\nExamples:\n impeccable detect src/\n impeccable detect index.html\n impeccable detect https://example.com\n impeccable detect --json .\n impeccable detect --no-config src/\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/blinking-cursor.html\n line 20: [design-system-color] Undocumented color #33d17a is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n line 58: [design-system-color] Undocumented color #999 is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n [pulsing-dot] .pass-round-dot — 8x8px dot with infinite \"blink-anim\" animation\n → Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer.\n\n3 anti-patterns found.\n",
|
||||
"stderr": "\n<REPO>/tests/fixtures/antipatterns/blinking-cursor.html\n [pulsing-dot] .pass-round-dot — 8x8px dot with infinite \"blink-anim\" animation\n → Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer.\n\n1 anti-pattern found.\n\n── Advisory (not counted as failures) ──\n\n<REPO>/tests/fixtures/antipatterns/blinking-cursor.html\n line 20: [design-system-color] Undocumented color #33d17a is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n line 58: [design-system-color] Undocumented color #999 is outside DESIGN.md colors\n → A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.\n\n2 advisory notes. Suppress with --no-advisory.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user