chore: update vendored skills and record provenance
humanizer 2.2.0 -> 2.9.1 (blader/humanizer, MIT): adds a Voice Calibration section and a passive-voice pattern, reworks negative parallelisms and em dashes. Version moved to metadata.version upstream. impeccable 3.6.0 -> 4.0.4 (pbakaus/impeccable, Apache-2.0): the repo tags the skill and the npm CLI separately, so 3.6.0 was a real release and npm's 3.5.0 was never the comparison. Adds native-platform reference briefs. Each now carries an UPSTREAM file; bin/check-vendored.sh reports drift.
This commit is contained in:
@@ -530,7 +530,11 @@ if (IS_BROWSER) {
|
||||
function generateSelector(el) {
|
||||
if (el === document.body) return 'body';
|
||||
if (el === document.documentElement) return 'html';
|
||||
if (el.id) return '#' + CSS.escape(el.id);
|
||||
// Read via getAttribute when `el.id` is not a string — a <form> with a
|
||||
// named control (e.g. <input name="id">) shadows the builtin getter and
|
||||
// returns the element, producing a garbage `#[object …]` selector (#407).
|
||||
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute('id') || '');
|
||||
if (elId) return '#' + CSS.escape(elId);
|
||||
|
||||
const parts = [];
|
||||
let current = el;
|
||||
@@ -1222,8 +1226,13 @@ if (IS_BROWSER) {
|
||||
return {
|
||||
type: f.type || f.id,
|
||||
category: ap ? ap.category : 'quality',
|
||||
severity: ap?.severity || 'warning',
|
||||
severity: f.severity || ap?.severity || 'warning',
|
||||
// Advisory findings (em-dash overuse, etc.) are surfaced but never
|
||||
// treated as failures; carry the flag so the overlay/extension can
|
||||
// render them with the mildest affordance and consumers can filter.
|
||||
advisory: (ap && ap.advisory === true) || f.advisory === true,
|
||||
detail: f.detail || f.snippet,
|
||||
ignoreValue: f.ignoreValue || f.value || '',
|
||||
name: ap ? ap.name : (f.type || f.id),
|
||||
description: ap ? ap.description : '',
|
||||
};
|
||||
@@ -1260,20 +1269,213 @@ if (IS_BROWSER) {
|
||||
return [...groupMap.entries()].map(([el, findings]) => ({ el, findings }));
|
||||
}
|
||||
|
||||
const DESIGN_COLOR_TOLERANCE = 6;
|
||||
const DESIGN_RADIUS_TOLERANCE_PX = 0.5;
|
||||
const DESIGN_SKIP_TAGS = new Set(['head', 'title', 'meta', 'link', 'style', 'script', 'noscript', 'template', 'source']);
|
||||
|
||||
function normalizeBrowserFontName(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function browserPrimaryFont(stack) {
|
||||
if (!stack || /var\(/i.test(stack)) return '';
|
||||
return String(stack || '')
|
||||
.split(',')
|
||||
.map(normalizeBrowserFontName)
|
||||
.find(font => font && !GENERIC_FONTS.has(font)) || '';
|
||||
}
|
||||
|
||||
function browserDesignSystemConfig() {
|
||||
const raw = window.__IMPECCABLE_CONFIG__?.designSystem;
|
||||
if (!raw?.present) return null;
|
||||
const allowedFonts = new Set((raw.allowedFonts || []).map(normalizeBrowserFontName).filter(Boolean));
|
||||
const allowedColors = (raw.allowedColors || [])
|
||||
.filter(color => color && Number.isFinite(color.r) && Number.isFinite(color.g) && Number.isFinite(color.b))
|
||||
.map(color => ({ r: color.r, g: color.g, b: color.b }));
|
||||
const allowedRadii = (raw.allowedRadii || [])
|
||||
.map(Number)
|
||||
.filter(px => Number.isFinite(px));
|
||||
return {
|
||||
present: true,
|
||||
hasFonts: raw.hasFonts === true && allowedFonts.size > 0,
|
||||
allowedFonts,
|
||||
hasColors: raw.hasColors === true && allowedColors.length > 0,
|
||||
allowedColors,
|
||||
hasRadii: raw.hasRadii === true && allowedRadii.length > 0,
|
||||
allowedRadii,
|
||||
hasPillRadius: raw.hasPillRadius === true,
|
||||
};
|
||||
}
|
||||
|
||||
function browserColorsClose(a, b) {
|
||||
if (!a || !b) return false;
|
||||
return Math.max(
|
||||
Math.abs(a.r - b.r),
|
||||
Math.abs(a.g - b.g),
|
||||
Math.abs(a.b - b.b),
|
||||
) <= DESIGN_COLOR_TOLERANCE;
|
||||
}
|
||||
|
||||
function isBrowserDesignColorAllowed(raw, designSystem) {
|
||||
if (!designSystem?.hasColors) return true;
|
||||
const text = String(raw || '').trim().toLowerCase();
|
||||
if (!text || text === 'transparent' || text === 'currentcolor' || text === 'inherit' || text === 'initial') return true;
|
||||
if (text.includes('var(')) return true;
|
||||
const parsed = parseAnyColor(text);
|
||||
if (!parsed) return true;
|
||||
if ((parsed.a ?? 1) <= 0.05) return true;
|
||||
return designSystem.allowedColors.some(color => browserColorsClose(parsed, color));
|
||||
}
|
||||
|
||||
function isBrowserTransparentCss(value) {
|
||||
const text = String(value || '').trim().toLowerCase();
|
||||
if (!text || text === 'transparent') return true;
|
||||
const parsed = parseAnyColor(text);
|
||||
return parsed ? (parsed.a ?? 1) <= 0.05 : false;
|
||||
}
|
||||
|
||||
function isBrowserDesignRadiusAllowed(raw, designSystem) {
|
||||
if (!designSystem?.hasRadii) return true;
|
||||
const text = String(raw || '').trim().toLowerCase();
|
||||
if (!text || text === '0' || text === 'none' || text === 'initial' || text === 'inherit') return true;
|
||||
if (text.includes('var(') || text.includes('%')) return true;
|
||||
const px = resolveLengthPx(text, 16);
|
||||
if (px == null || !Number.isFinite(px) || px <= DESIGN_RADIUS_TOLERANCE_PX) return true;
|
||||
if (designSystem.hasPillRadius && px >= 99) return true;
|
||||
return designSystem.allowedRadii.some(allowed => Math.abs(allowed - px) <= DESIGN_RADIUS_TOLERANCE_PX);
|
||||
}
|
||||
|
||||
function browserRadiusTokens(value) {
|
||||
return String(value || '')
|
||||
.replace(/\s*\/\s*/g, ' ')
|
||||
.split(/\s+/)
|
||||
.map(token => token.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function browserHasDirectText(el) {
|
||||
return [...(el.childNodes || [])].some(node => node.nodeType === 3 && node.textContent.trim().length > 0);
|
||||
}
|
||||
|
||||
function browserSampleText(el) {
|
||||
const text = String(el.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
return text ? ` "${text.slice(0, 40)}"` : '';
|
||||
}
|
||||
|
||||
function shouldSkipDesignElement(el) {
|
||||
const tag = el.tagName?.toLowerCase?.() || '';
|
||||
return DESIGN_SKIP_TAGS.has(tag) || isElementHidden(el);
|
||||
}
|
||||
|
||||
function checkElementDesignSystemDOM(el, designSystem, seen) {
|
||||
if (!designSystem?.present || shouldSkipDesignElement(el)) return [];
|
||||
const findings = [];
|
||||
const tag = el.tagName?.toLowerCase?.() || 'unknown';
|
||||
const style = getComputedStyle(el);
|
||||
|
||||
if (designSystem.hasFonts && browserHasDirectText(el)) {
|
||||
const font = browserPrimaryFont(style.fontFamily || '');
|
||||
if (font && !designSystem.allowedFonts.has(font) && !seen.fonts.has(font)) {
|
||||
seen.fonts.add(font);
|
||||
findings.push({
|
||||
type: 'design-system-font',
|
||||
detail: `${tag}${browserSampleText(el)} uses ${font}; not declared in DESIGN.md typography`,
|
||||
ignoreValue: font,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (designSystem.hasColors) {
|
||||
const colorChecks = [];
|
||||
if (browserHasDirectText(el)) colorChecks.push(['text color', style.color]);
|
||||
if (!isBrowserTransparentCss(style.backgroundColor)) colorChecks.push(['background', style.backgroundColor]);
|
||||
for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
|
||||
if ((parseFloat(style[`border${side}Width`]) || 0) > 0) {
|
||||
colorChecks.push([`border-${side.toLowerCase()}`, style[`border${side}Color`]]);
|
||||
}
|
||||
}
|
||||
if ((parseFloat(style.outlineWidth) || 0) > 0) colorChecks.push(['outline', style.outlineColor]);
|
||||
|
||||
for (const [kind, raw] of colorChecks) {
|
||||
const label = String(raw || '').trim().replace(/\s+/g, ' ');
|
||||
if (isBrowserDesignColorAllowed(label, designSystem)) continue;
|
||||
const key = `${kind}:${label}`;
|
||||
if (seen.colors.has(key)) continue;
|
||||
seen.colors.add(key);
|
||||
findings.push({
|
||||
type: 'design-system-color',
|
||||
detail: `${kind} ${label} on ${tag}${browserSampleText(el)} is outside DESIGN.md colors`,
|
||||
ignoreValue: label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (designSystem.hasRadii) {
|
||||
for (const token of browserRadiusTokens(style.borderRadius || '')) {
|
||||
if (isBrowserDesignRadiusAllowed(token, designSystem)) continue;
|
||||
if (seen.radii.has(token)) continue;
|
||||
seen.radii.add(token);
|
||||
findings.push({
|
||||
type: 'design-system-radius',
|
||||
detail: `border-radius ${token} on ${tag}${browserSampleText(el)} is outside the DESIGN.md rounded scale`,
|
||||
ignoreValue: token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
function decodeBrowserGoogleFamily(value) {
|
||||
const family = String(value || '').split(':')[0].replace(/\+/g, ' ');
|
||||
try {
|
||||
return decodeURIComponent(family);
|
||||
} catch {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
function checkBrowserDesignSystemSources(designSystem, seen) {
|
||||
if (!designSystem?.hasFonts) return [];
|
||||
const findings = [];
|
||||
for (const link of document.querySelectorAll('link[href*="fonts.googleapis.com/css"]')) {
|
||||
const href = link.getAttribute('href') || '';
|
||||
for (const match of href.matchAll(/[?&]family=([^&]+)/g)) {
|
||||
const display = decodeBrowserGoogleFamily(match[1]);
|
||||
const font = normalizeBrowserFontName(display);
|
||||
if (!font || designSystem.allowedFonts.has(font) || seen.fonts.has(font)) continue;
|
||||
seen.fonts.add(font);
|
||||
findings.push({
|
||||
type: 'design-system-font',
|
||||
detail: `Google Fonts: ${display} is not declared in DESIGN.md typography`,
|
||||
ignoreValue: display,
|
||||
});
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function collectBrowserFindings() {
|
||||
const groupMap = new Map();
|
||||
const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : [];
|
||||
const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
|
||||
// Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a
|
||||
// real browser env (detector page, live overlay, extension) running every
|
||||
// check is free, so we always surface them; the gating is purely a CLI
|
||||
// output concern, applied in the Node engines' detect* return paths.
|
||||
const designSystem = browserDesignSystemConfig();
|
||||
const designSeen = { fonts: new Set(), colors: new Set(), radii: new Set() };
|
||||
// All deterministic rules run in the browser and extension path.
|
||||
|
||||
for (const el of document.querySelectorAll('*')) {
|
||||
// Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons)
|
||||
if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
|
||||
// Skip browser extension elements (Claude, etc.)
|
||||
const elId = el.id || '';
|
||||
// Skip browser extension elements (Claude, etc.). Use getAttribute when
|
||||
// `el.id` is not a string: a <form> with a named control like
|
||||
// <input name="id"> shadows the builtin `id` getter and returns the
|
||||
// element, whose `.startsWith` throws (issue #407).
|
||||
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute('id') || '');
|
||||
if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
|
||||
// Skip the impeccable live-mode overlay (highlight, tooltip, bar, picker, toast).
|
||||
// These are inspector chrome, not part of the user's design.
|
||||
@@ -1283,10 +1485,12 @@ if (IS_BROWSER) {
|
||||
|
||||
const findings = [
|
||||
...checkElementBordersDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementPseudoStripeDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementColorsDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
@@ -1294,6 +1498,8 @@ if (IS_BROWSER) {
|
||||
...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementBlinkingCursorDOM(el).map(f => ({ type: f.id, detail: f.snippet, ...(f.severity ? { severity: f.severity } : {}) })),
|
||||
...checkElementDesignSystemDOM(el, designSystem, designSeen),
|
||||
].filter(f => _ruleOk(f.type));
|
||||
|
||||
addBrowserFindings(groupMap, el, findings);
|
||||
@@ -1310,13 +1516,20 @@ if (IS_BROWSER) {
|
||||
|
||||
const pageLevelFindings = [];
|
||||
|
||||
const designSourceFindings = checkBrowserDesignSystemSources(designSystem, designSeen)
|
||||
.filter(f => _ruleOk(f.type));
|
||||
if (designSourceFindings.length > 0) {
|
||||
pageLevelFindings.push(...designSourceFindings);
|
||||
addBrowserFindings(groupMap, document.body, designSourceFindings);
|
||||
}
|
||||
|
||||
const typoFindings = checkTypography().filter(f => _ruleOk(f.type));
|
||||
if (typoFindings.length > 0) {
|
||||
pageLevelFindings.push(...typoFindings);
|
||||
addBrowserFindings(groupMap, document.body, typoFindings);
|
||||
}
|
||||
|
||||
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
|
||||
const sectionKickerFindings = checkKickerAboveHeadingDOM()
|
||||
.map(f => ({ type: f.id, detail: f.snippet }))
|
||||
.filter(f => _ruleOk(f.type));
|
||||
if (sectionKickerFindings.length > 0) {
|
||||
@@ -1324,12 +1537,66 @@ if (IS_BROWSER) {
|
||||
addBrowserFindings(groupMap, document.body, sectionKickerFindings);
|
||||
}
|
||||
|
||||
const numberedLabelFindings = checkNumberedSectionLabelsDOM()
|
||||
.map(f => ({ type: f.id, detail: f.snippet }))
|
||||
.filter(f => _ruleOk(f.type));
|
||||
if (numberedLabelFindings.length > 0) {
|
||||
pageLevelFindings.push(...numberedLabelFindings);
|
||||
addBrowserFindings(groupMap, document.body, numberedLabelFindings);
|
||||
}
|
||||
|
||||
const repeatedTextFindings = checkRepeatedContainerTextDOM()
|
||||
.map(f => ({ type: f.id, detail: f.snippet }))
|
||||
.filter(f => _ruleOk(f.type));
|
||||
if (repeatedTextFindings.length > 0) {
|
||||
pageLevelFindings.push(...repeatedTextFindings);
|
||||
addBrowserFindings(groupMap, document.body, repeatedTextFindings);
|
||||
}
|
||||
|
||||
// Em-dash overuse (advisory): browser parity with the static/regex path.
|
||||
// Reads rendered body text so it catches dashes written as HTML entities.
|
||||
// serializeFindings stamps the advisory flag from the registry.
|
||||
const emDashFindings = checkEmDashOveruseDOM()
|
||||
.map(f => ({ type: f.id, detail: f.snippet }))
|
||||
.filter(f => _ruleOk(f.type));
|
||||
if (emDashFindings.length > 0) {
|
||||
pageLevelFindings.push(...emDashFindings);
|
||||
addBrowserFindings(groupMap, document.body, emDashFindings);
|
||||
}
|
||||
|
||||
const layoutFindings = checkLayout().filter(f => _ruleOk(f.type));
|
||||
for (const f of layoutFindings) {
|
||||
const el = f.el || document.body;
|
||||
addBrowserFindings(groupMap, el, [{ type: f.type, detail: f.detail || f.snippet }]);
|
||||
}
|
||||
|
||||
// Heading rhythm (browser-only: needs real layout for the gap math)
|
||||
const headingRhythmFindings = checkHeadingRhythmDOM().filter(f => _ruleOk(f.type));
|
||||
for (const f of headingRhythmFindings) {
|
||||
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
||||
}
|
||||
|
||||
// Edge-flush cards in horizontal scrollers (browser-only: needs real
|
||||
// layout for the scroller clip box vs card rect math)
|
||||
const edgeFlushFindings = checkEdgeFlushCardsDOM().filter(f => _ruleOk(f.type));
|
||||
for (const f of edgeFlushFindings) {
|
||||
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
||||
}
|
||||
|
||||
// Text occlusion / element overlap (browser-only: needs real layout +
|
||||
// elementFromPoint to confirm what actually paints on top)
|
||||
const occlusionFindings = checkTextOcclusionDOM().filter(f => _ruleOk(f.type));
|
||||
for (const f of occlusionFindings) {
|
||||
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
||||
}
|
||||
|
||||
// First-viewport column overflow — the stretched-hero signature
|
||||
// (browser-only: needs real layout for the content-extent math)
|
||||
const colOverflowFindings = checkFirstViewportColumnOverflowDOM().filter(f => _ruleOk(f.type));
|
||||
for (const f of colOverflowFindings) {
|
||||
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
||||
}
|
||||
|
||||
// Page-level quality checks (headings, etc.)
|
||||
const qualityFindings = checkPageQualityDOM().filter(f => _ruleOk(f.type));
|
||||
if (qualityFindings.length > 0) {
|
||||
@@ -1355,7 +1622,25 @@ if (IS_BROWSER) {
|
||||
}
|
||||
const htmlPatternFindings = checkHtmlPatterns(docClone.outerHTML);
|
||||
if (htmlPatternFindings.length > 0) {
|
||||
const mapped = htmlPatternFindings.map(f => ({ type: f.id, detail: f.snippet })).filter(f => _ruleOk(f.type));
|
||||
const mapped = htmlPatternFindings.map(f => {
|
||||
const item = { type: f.id, detail: f.snippet };
|
||||
if (f.severity) {
|
||||
item.severity = f.severity;
|
||||
} else if (f.id === 'pulsing-dot' && f.selector) {
|
||||
// The string scan promotes header/nav dots on its own; with a live
|
||||
// layout also promote dots resting in the first ~900px of the page
|
||||
// (the hero region), which the source scan cannot measure.
|
||||
try {
|
||||
const dotEl = document.querySelector(f.selector);
|
||||
if (dotEl) {
|
||||
const rect = dotEl.getBoundingClientRect();
|
||||
const pageTop = rect.top + (window.scrollY || 0);
|
||||
if (pageTop <= 900) item.severity = 'error';
|
||||
}
|
||||
} catch { /* unresolvable selector: keep registry severity */ }
|
||||
}
|
||||
return item;
|
||||
}).filter(f => _ruleOk(f.type));
|
||||
pageLevelFindings.push(...mapped);
|
||||
addBrowserFindings(groupMap, document.body, mapped);
|
||||
}
|
||||
@@ -1729,6 +2014,9 @@ if (IS_BROWSER) {
|
||||
window.impeccableDetectAsync = detectAsync;
|
||||
window.impeccableScan = scan;
|
||||
window.impeccableScanAsync = scanAsync;
|
||||
// Raw measurement for the URL engine's content-hidden-at-rest pass: it
|
||||
// drives a reveal sweep from Node and thresholds the result itself.
|
||||
window.impeccableMeasureHiddenText = measureHiddenTextDOM;
|
||||
window.impeccableCollectVisualContrastCandidates = collectVisualContrastCandidates;
|
||||
window.impeccableAnalyzeVisualContrast = analyzeVisualContrast;
|
||||
window.impeccableGetLastVisualContrastAnalyses = () => lastVisualContrastAnalyses.slice();
|
||||
|
||||
Reference in New Issue
Block a user