quiver-cli 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
- package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
- package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
- package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
- package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
- package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
- package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
- package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
- package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
- package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
- package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
- package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
- package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
- package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
- package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
- package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
- package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
- package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
- package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
- package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
- package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
- package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
- package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
- package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
- package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
- package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
- package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
- package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
- package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
- package/template/.agents/skills/hono/SKILL.md +17 -3
- package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
- package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
- package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
- package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
- package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
- package/template/.agents/skills/supabase/SKILL.md +5 -1
- package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
- package/template/.agents/skills/writing/humanizer/README.md +209 -0
- package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
- package/template/.agents/upstreams.json +21 -22
- package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
|
@@ -18,4 +18,13 @@ if (!detectorPath) {
|
|
|
18
18
|
|
|
19
19
|
const { detectCli } = await import(pathToFileURL(detectorPath));
|
|
20
20
|
|
|
21
|
+
// A comp-led build with its comp round or hero gate still open is not a page
|
|
22
|
+
// the detector can pass: say so after the scan (stderr, so --json stays
|
|
23
|
+
// parseable), on the same condition context.mjs reports at boot.
|
|
24
|
+
try {
|
|
25
|
+
const { compRoundOpen } = await import(pathToFileURL(path.join(__dirname, 'build-phase.mjs')));
|
|
26
|
+
const open = compRoundOpen(process.cwd());
|
|
27
|
+
if (open) process.stderr.write(`COMP_ROUND_OPEN: ${open.reason}. A detector pass is not a finish: run node ${__dirname}/build-phase.mjs status and follow its NEXT line before treating this page as built.\n`);
|
|
28
|
+
} catch { /* build-phase absent */ }
|
|
29
|
+
|
|
21
30
|
await detectCli();
|
package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs
CHANGED
|
@@ -626,7 +626,7 @@ if (IS_BROWSER) {
|
|
|
626
626
|
if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
|
|
627
627
|
if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
|
|
628
628
|
|
|
629
|
-
const solidBg = parseRgb(currentStyle.backgroundColor);
|
|
629
|
+
const solidBg = parseRgb(currentStyle.backgroundColor) || parseAnyColor(currentStyle.backgroundColor);
|
|
630
630
|
if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
|
|
631
631
|
current = current.parentElement;
|
|
632
632
|
}
|
|
@@ -683,8 +683,12 @@ if (IS_BROWSER) {
|
|
|
683
683
|
|
|
684
684
|
const reasons = collectVisualContrastReasons(el, style);
|
|
685
685
|
if (reasons.length === 0) continue;
|
|
686
|
+
// Image-only mode filters here, inside the cap: gradient/opacity/filter
|
|
687
|
+
// candidates earlier in DOM order must not consume the budget and
|
|
688
|
+
// starve the url()-backed texts this mode exists to sample.
|
|
689
|
+
if (options.imageOnly && !reasons.includes('image background')) continue;
|
|
686
690
|
|
|
687
|
-
const textColor = parseRgb(style.color);
|
|
691
|
+
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
|
688
692
|
const fontSize = parseFloat(style.fontSize) || 16;
|
|
689
693
|
const fontWeight = parseInt(style.fontWeight) || 400;
|
|
690
694
|
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
|
@@ -981,7 +985,7 @@ if (IS_BROWSER) {
|
|
|
981
985
|
return sample;
|
|
982
986
|
}
|
|
983
987
|
}
|
|
984
|
-
const bg = parseRgb(style.backgroundColor);
|
|
988
|
+
const bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
|
|
985
989
|
if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
|
|
986
990
|
return { status: 'unresolved', reason: 'no readable background' };
|
|
987
991
|
}
|
|
@@ -1111,7 +1115,7 @@ if (IS_BROWSER) {
|
|
|
1111
1115
|
}
|
|
1112
1116
|
|
|
1113
1117
|
const style = getComputedStyle(el);
|
|
1114
|
-
const textColor = parseRgb(style.color) || candidate.textColor;
|
|
1118
|
+
const textColor = parseRgb(style.color) || parseAnyColor(style.color) || candidate.textColor;
|
|
1115
1119
|
if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
|
|
1116
1120
|
|
|
1117
1121
|
const rect = getDirectTextRect(el) || el.getBoundingClientRect();
|
|
@@ -1175,6 +1179,7 @@ if (IS_BROWSER) {
|
|
|
1175
1179
|
}
|
|
1176
1180
|
|
|
1177
1181
|
async function analyzeVisualContrast(options = {}) {
|
|
1182
|
+
// imageOnly is enforced inside the collector, before the candidate cap.
|
|
1178
1183
|
const candidates = collectVisualContrastCandidates(options);
|
|
1179
1184
|
const results = [];
|
|
1180
1185
|
const shouldScrollOffscreen = options.scrollOffscreen === true;
|
|
@@ -1260,9 +1265,16 @@ if (IS_BROWSER) {
|
|
|
1260
1265
|
|
|
1261
1266
|
function addBrowserFindings(groupMap, el, findings) {
|
|
1262
1267
|
if (!findings || findings.length === 0) return;
|
|
1268
|
+
// Element-scoped waivers: a data-impeccable-ignore ancestor suppresses
|
|
1269
|
+
// matching findings for its whole subtree. Applied at this choke point so
|
|
1270
|
+
// every per-element attribution (checks, layout, occlusion, rhythm)
|
|
1271
|
+
// honors it; page-level findings attributed to <body> pass through
|
|
1272
|
+
// untouched, since body has no ignoring ancestor.
|
|
1273
|
+
const kept = findings.filter(f => !scopedIgnoreActive(el, f.type));
|
|
1274
|
+
if (kept.length === 0) return;
|
|
1263
1275
|
const existing = groupMap.get(el);
|
|
1264
|
-
if (existing) existing.push(...
|
|
1265
|
-
else groupMap.set(el, [...
|
|
1276
|
+
if (existing) existing.push(...kept);
|
|
1277
|
+
else groupMap.set(el, [...kept]);
|
|
1266
1278
|
}
|
|
1267
1279
|
|
|
1268
1280
|
function browserFindingsFromMap(groupMap) {
|
|
@@ -1460,7 +1472,19 @@ if (IS_BROWSER) {
|
|
|
1460
1472
|
return findings;
|
|
1461
1473
|
}
|
|
1462
1474
|
|
|
1475
|
+
// A page matched by detector.ignoreFiles is waived wholesale: every scan
|
|
1476
|
+
// stage answers empty so the badge and toast read zero. Mirrors
|
|
1477
|
+
// shouldIgnoreDetectionFile in cli/lib/impeccable-config.mjs; the live
|
|
1478
|
+
// overlay resolves the globs per page (live-browser-ignores.js) and
|
|
1479
|
+
// forwards the verdict as config.skipScan.
|
|
1480
|
+
function skipScanActive() {
|
|
1481
|
+
return EXTENSION_MODE && window.__IMPECCABLE_CONFIG__?.skipScan === true;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1463
1484
|
function collectBrowserFindings() {
|
|
1485
|
+
if (skipScanActive()) {
|
|
1486
|
+
return { groupMap: new Map(), allFindings: [], pageLevelFindings: [] };
|
|
1487
|
+
}
|
|
1464
1488
|
const groupMap = new Map();
|
|
1465
1489
|
const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : [];
|
|
1466
1490
|
const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
|
|
@@ -1620,9 +1644,27 @@ if (IS_BROWSER) {
|
|
|
1620
1644
|
for (const node of docClone.querySelectorAll('[id^="impeccable-live-"]')) {
|
|
1621
1645
|
node.remove();
|
|
1622
1646
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1647
|
+
// Regex findings that name a live selector resolve against the real DOM:
|
|
1648
|
+
// pseudo-element/class segments are stripped (the host element is the
|
|
1649
|
+
// anchor), a selector that matches nothing on this page drops the finding
|
|
1650
|
+
// (the CSS ships here, but the pattern never renders — the live DOM is
|
|
1651
|
+
// ground truth in the browser), and a match under a data-impeccable-ignore
|
|
1652
|
+
// ancestor is waived. Selector-less findings stay page-level.
|
|
1653
|
+
const scopedHtmlFindings = checkHtmlPatterns(docClone.outerHTML).filter(f => {
|
|
1654
|
+
if (!f.selector) return true;
|
|
1655
|
+
const query = String(f.selector).replace(/::?[a-zA-Z-]+(\([^)]*\))?/g, '').trim().replace(/,\s*(?=,|$)/g, '');
|
|
1656
|
+
if (!query || /^[,\s]*$/.test(query)) return true;
|
|
1657
|
+
let matches;
|
|
1658
|
+
try {
|
|
1659
|
+
matches = document.querySelectorAll(query);
|
|
1660
|
+
} catch {
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
if (matches.length === 0) return false;
|
|
1664
|
+
return [...matches].some(el => !scopedIgnoreActive(el, f.id));
|
|
1665
|
+
});
|
|
1666
|
+
if (scopedHtmlFindings.length > 0) {
|
|
1667
|
+
const mapped = scopedHtmlFindings.map(f => {
|
|
1626
1668
|
const item = { type: f.id, detail: f.snippet };
|
|
1627
1669
|
if (f.severity) {
|
|
1628
1670
|
item.severity = f.severity;
|
|
@@ -1645,6 +1687,119 @@ if (IS_BROWSER) {
|
|
|
1645
1687
|
addBrowserFindings(groupMap, document.body, mapped);
|
|
1646
1688
|
}
|
|
1647
1689
|
|
|
1690
|
+
// Value-level suppression (issue #639). `disabledRules` above handles
|
|
1691
|
+
// whole rules; this applies the config's remaining ignoreValues entries,
|
|
1692
|
+
// which the CLI filters through isIgnoredFindingValue in
|
|
1693
|
+
// cli/lib/impeccable-config.mjs, so a project waiver like
|
|
1694
|
+
// overused-font = "geist mono" reaches the overlay and extension too.
|
|
1695
|
+
const _normValue = (v) => String(v || '').trim().replace(/^["']|["']$/g, '')
|
|
1696
|
+
.replace(/\+/g, ' ').replace(/\s+/g, ' ').toLowerCase();
|
|
1697
|
+
const _disabledValues = EXTENSION_MODE
|
|
1698
|
+
? (Array.isArray(window.__IMPECCABLE_CONFIG__?.disabledValues) ? window.__IMPECCABLE_CONFIG__.disabledValues : [])
|
|
1699
|
+
.filter(e => e && typeof e === 'object' && e.rule && e.value)
|
|
1700
|
+
.map(e => ({ rule: String(e.rule).trim().toLowerCase(), value: _normValue(e.value) }))
|
|
1701
|
+
: [];
|
|
1702
|
+
if (_disabledValues.length > 0) {
|
|
1703
|
+
// The six rules whose findings carry a matchable value; keep in step
|
|
1704
|
+
// with extractFindingIgnoreValue in cli/lib/impeccable-config.mjs.
|
|
1705
|
+
// Everything else is suppressed by rule or by file scope, both already
|
|
1706
|
+
// resolved into disabledRules before the scan message was sent.
|
|
1707
|
+
const _directValueRules = new Set([
|
|
1708
|
+
'overused-font',
|
|
1709
|
+
'bounce-easing',
|
|
1710
|
+
'design-system-font',
|
|
1711
|
+
'design-system-color',
|
|
1712
|
+
'design-system-radius',
|
|
1713
|
+
'design-system-font-size',
|
|
1714
|
+
]);
|
|
1715
|
+
// The design-system checks set `ignoreValue` on their findings; the
|
|
1716
|
+
// detail fallbacks catch overused-font, whose value lives in its
|
|
1717
|
+
// sentence. One CLI matcher is not mirrored here: the motion extractor
|
|
1718
|
+
// (a value-scoped bounce-easing waiver only matches when the finding
|
|
1719
|
+
// carries ignoreValue directly). The CLI's [?&]family= URL fallback is
|
|
1720
|
+
// also omitted on purpose: browser findings for these rules always
|
|
1721
|
+
// carry ignoreValue or a "Primary font:" / "Google Fonts:" /
|
|
1722
|
+
// font-family sentence, so it is unreachable here.
|
|
1723
|
+
const _findingValue = (f) => {
|
|
1724
|
+
if (!f || !_directValueRules.has(f.type || f.id)) return '';
|
|
1725
|
+
const direct = f.ignoreValue || f.value;
|
|
1726
|
+
if (direct) return _normValue(direct);
|
|
1727
|
+
// The CLI routes bounce-easing through extractMotionIgnoreValue and
|
|
1728
|
+
// never the font regexes; without a direct ignoreValue there is no
|
|
1729
|
+
// value to match, so do not invent one from unrelated CSS text.
|
|
1730
|
+
if ((f.type || f.id) === 'bounce-easing') return '';
|
|
1731
|
+
for (const text of [f.detail, f.snippet]) {
|
|
1732
|
+
if (typeof text !== 'string' || !text) continue;
|
|
1733
|
+
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
|
1734
|
+
if (primary) return _normValue(primary[1]);
|
|
1735
|
+
const google = text.match(/Google Fonts:\s*([^()\n;]+)/i);
|
|
1736
|
+
if (google) return _normValue(google[1]);
|
|
1737
|
+
const family = text.match(/font-family\s*:\s*["']?([^'",;\n]+)/i);
|
|
1738
|
+
if (family) return _normValue(family[1]);
|
|
1739
|
+
}
|
|
1740
|
+
return '';
|
|
1741
|
+
};
|
|
1742
|
+
// design-system-color compares by color value, not by spelling: the
|
|
1743
|
+
// browser reports computed rgb(...) strings while waivers are usually
|
|
1744
|
+
// written as hex. Mirrors ignoreValueMatches -> colorIgnoreKey in
|
|
1745
|
+
// cli/lib/impeccable-config.mjs for the hex and rgb()/rgba() forms;
|
|
1746
|
+
// hsl stays CLI-only.
|
|
1747
|
+
const _colorKey = (value) => {
|
|
1748
|
+
const text = String(value || '').trim().toLowerCase();
|
|
1749
|
+
const hex = text.match(/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/);
|
|
1750
|
+
if (hex) {
|
|
1751
|
+
const expanded = hex[1].length <= 4 ? [...hex[1]].map(d => d + d).join('') : hex[1];
|
|
1752
|
+
const [r, g, b, a = 255] = expanded.match(/../g).map(ch => parseInt(ch, 16));
|
|
1753
|
+
return `${r},${g},${b},${a}`;
|
|
1754
|
+
}
|
|
1755
|
+
const rgb = text.match(/^rgba?\((.*)\)$/);
|
|
1756
|
+
if (!rgb) return '';
|
|
1757
|
+
const body = rgb[1].trim().replace(/\s*\/\s*/g, ' / ');
|
|
1758
|
+
let parts;
|
|
1759
|
+
if (body.includes(',')) {
|
|
1760
|
+
parts = body.split(',').map(p => p.trim()).filter(Boolean);
|
|
1761
|
+
const last = parts[parts.length - 1];
|
|
1762
|
+
if (last && last.includes('/')) {
|
|
1763
|
+
parts = [...parts.slice(0, -1), ...last.split('/').map(p => p.trim()).filter(Boolean)];
|
|
1764
|
+
}
|
|
1765
|
+
} else {
|
|
1766
|
+
parts = body.split(/\s+/).filter(p => p && p !== '/');
|
|
1767
|
+
}
|
|
1768
|
+
if (parts.length < 3 || parts.length > 4) return '';
|
|
1769
|
+
const channel = (raw, isAlpha) => {
|
|
1770
|
+
const m = String(raw).trim().match(/^(-?\d*\.?\d+)(%)?$/);
|
|
1771
|
+
if (!m) return null;
|
|
1772
|
+
let v = parseFloat(m[1]);
|
|
1773
|
+
if (m[2]) v = isAlpha ? v / 100 : v * 2.55;
|
|
1774
|
+
const max = isAlpha ? 1 : 255;
|
|
1775
|
+
if (!Number.isFinite(v) || v < 0 || v > max) return null;
|
|
1776
|
+
return isAlpha ? v : Math.round(v);
|
|
1777
|
+
};
|
|
1778
|
+
const r = channel(parts[0], false);
|
|
1779
|
+
const g = channel(parts[1], false);
|
|
1780
|
+
const b = channel(parts[2], false);
|
|
1781
|
+
const a = parts[3] === undefined ? 1 : channel(parts[3], true);
|
|
1782
|
+
if ([r, g, b, a].some(v => v === null)) return '';
|
|
1783
|
+
return `${r},${g},${b},${Math.round(a * 255)}`;
|
|
1784
|
+
};
|
|
1785
|
+
const _valueIgnored = (f) => {
|
|
1786
|
+
const value = _findingValue(f);
|
|
1787
|
+
if (!value) return false;
|
|
1788
|
+
const rule = f.type || f.id;
|
|
1789
|
+
return _disabledValues.some(e => e.rule === rule && (e.value === value
|
|
1790
|
+
|| (rule === 'design-system-color'
|
|
1791
|
+
&& _colorKey(e.value) !== '' && _colorKey(e.value) === _colorKey(value))));
|
|
1792
|
+
};
|
|
1793
|
+
for (const [el, list] of [...groupMap.entries()]) {
|
|
1794
|
+
const kept = list.filter(f => !_valueIgnored(f));
|
|
1795
|
+
if (kept.length > 0) groupMap.set(el, kept);
|
|
1796
|
+
else groupMap.delete(el);
|
|
1797
|
+
}
|
|
1798
|
+
for (let i = pageLevelFindings.length - 1; i >= 0; i--) {
|
|
1799
|
+
if (_valueIgnored(pageLevelFindings[i])) pageLevelFindings.splice(i, 1);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1648
1803
|
return {
|
|
1649
1804
|
groupMap,
|
|
1650
1805
|
allFindings: browserFindingsFromMap(groupMap),
|
|
@@ -1652,8 +1807,27 @@ if (IS_BROWSER) {
|
|
|
1652
1807
|
};
|
|
1653
1808
|
}
|
|
1654
1809
|
|
|
1810
|
+
// Visual contrast has three modes. Explicit true runs the full sampled
|
|
1811
|
+
// pass; explicit false disables it entirely (the deterministic-only mode
|
|
1812
|
+
// the test suites use). Unset — the default overlay run — samples ONLY
|
|
1813
|
+
// image-backed text: the one class the analytic walk deliberately skips,
|
|
1814
|
+
// because a url() layer's pixels are unknowable without looking. In-page
|
|
1815
|
+
// sampling draws the source image alone to a canvas (glyph ink never
|
|
1816
|
+
// pollutes it), and a cross-origin image without CORS reports unresolved
|
|
1817
|
+
// instead of guessing.
|
|
1818
|
+
function visualContrastMode(options = {}) {
|
|
1819
|
+
const explicit = typeof options.visualContrast === 'boolean'
|
|
1820
|
+
? options.visualContrast
|
|
1821
|
+
: typeof window.__IMPECCABLE_CONFIG__?.visualContrast === 'boolean'
|
|
1822
|
+
? window.__IMPECCABLE_CONFIG__.visualContrast
|
|
1823
|
+
: null;
|
|
1824
|
+
if (explicit === true) return 'full';
|
|
1825
|
+
if (explicit === false) return false;
|
|
1826
|
+
return 'image-only';
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1655
1829
|
function shouldRunVisualContrast(options = {}) {
|
|
1656
|
-
return options
|
|
1830
|
+
return visualContrastMode(options) !== false;
|
|
1657
1831
|
}
|
|
1658
1832
|
|
|
1659
1833
|
function visualContrastOptions(options = {}) {
|
|
@@ -1830,6 +2004,7 @@ if (IS_BROWSER) {
|
|
|
1830
2004
|
return [];
|
|
1831
2005
|
}
|
|
1832
2006
|
const resolvedOptions = visualContrastOptions(options);
|
|
2007
|
+
if (visualContrastMode(options) === 'image-only') resolvedOptions.imageOnly = true;
|
|
1833
2008
|
const analyses = await analyzeVisualContrast(resolvedOptions);
|
|
1834
2009
|
if (runtime.generation && runtime.generation !== scanGeneration) return analyses;
|
|
1835
2010
|
lastVisualContrastAnalyses = analyses;
|
|
@@ -1842,6 +2017,12 @@ if (IS_BROWSER) {
|
|
|
1842
2017
|
|
|
1843
2018
|
async function collectBrowserFindingsAsync(options = {}, runtime = {}) {
|
|
1844
2019
|
const collected = collectBrowserFindings();
|
|
2020
|
+
// The visual pass walks the DOM on its own; on a skipScan page it would
|
|
2021
|
+
// repopulate the emptied scan, so it is skipped with everything else.
|
|
2022
|
+
if (skipScanActive()) {
|
|
2023
|
+
lastVisualContrastAnalyses = [];
|
|
2024
|
+
return { ...collected, allFindings: [], visualContrastAnalyses: [] };
|
|
2025
|
+
}
|
|
1845
2026
|
await addVisualContrastFindings(collected.groupMap, options, runtime);
|
|
1846
2027
|
return {
|
|
1847
2028
|
...collected,
|
|
@@ -1895,7 +2076,7 @@ if (IS_BROWSER) {
|
|
|
1895
2076
|
const generation = scanGeneration;
|
|
1896
2077
|
const collected = collectBrowserFindings();
|
|
1897
2078
|
const allFindings = renderBrowserFindings(collected, options);
|
|
1898
|
-
if (shouldRunVisualContrast(options)) {
|
|
2079
|
+
if (!skipScanActive() && shouldRunVisualContrast(options)) {
|
|
1899
2080
|
addVisualContrastFindings(collected.groupMap, options, { decorate: true, generation })
|
|
1900
2081
|
.then(() => {
|
|
1901
2082
|
if (generation === scanGeneration) postSerializedFindings(collected.groupMap, options);
|
|
@@ -105,6 +105,13 @@ function formatFindings(findings, jsonMode) {
|
|
|
105
105
|
// `optionsFor` maps a local path to scan options carrying that path's own
|
|
106
106
|
// project design system (or base options when null). Falls back to a plain
|
|
107
107
|
// object so direct/legacy callers still work.
|
|
108
|
+
async function detectLocalFile(filePath, options) {
|
|
109
|
+
if (HTML_EXTENSIONS.has(path.extname(filePath).toLowerCase())) {
|
|
110
|
+
return detectHtml(filePath, options);
|
|
111
|
+
}
|
|
112
|
+
return detectText(fs.readFileSync(filePath, 'utf-8'), filePath, options);
|
|
113
|
+
}
|
|
114
|
+
|
|
108
115
|
async function handleStdin(optionsFor = () => ({})) {
|
|
109
116
|
const resolve = typeof optionsFor === 'function' ? optionsFor : () => optionsFor;
|
|
110
117
|
const chunks = [];
|
|
@@ -114,9 +121,7 @@ async function handleStdin(optionsFor = () => ({})) {
|
|
|
114
121
|
const parsed = JSON.parse(input);
|
|
115
122
|
const fp = parsed?.tool_input?.file_path;
|
|
116
123
|
if (fp && fs.existsSync(fp)) {
|
|
117
|
-
|
|
118
|
-
return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase())
|
|
119
|
-
? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options);
|
|
124
|
+
return detectLocalFile(fp, resolve(fp));
|
|
120
125
|
}
|
|
121
126
|
} catch { /* not JSON */ }
|
|
122
127
|
return detectText(input, '<stdin>', resolve(null));
|
|
@@ -374,16 +379,10 @@ async function detectCli() {
|
|
|
374
379
|
}
|
|
375
380
|
|
|
376
381
|
for (const file of files) {
|
|
377
|
-
const ext = path.extname(file).toLowerCase();
|
|
378
382
|
// Each file resolves its own project design system (cached by root),
|
|
379
383
|
// so a scan spanning sibling projects applies the right rules per file.
|
|
380
384
|
const fileOptions = scanOptionsFor(file);
|
|
381
|
-
|
|
382
|
-
if (HTML_EXTENSIONS.has(ext)) {
|
|
383
|
-
fileFindings = await detectHtml(file, fileOptions);
|
|
384
|
-
} else {
|
|
385
|
-
fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, fileOptions);
|
|
386
|
-
}
|
|
385
|
+
const fileFindings = await detectLocalFile(file, fileOptions);
|
|
387
386
|
// Annotate findings with import context
|
|
388
387
|
const importers = importedByMap.get(file);
|
|
389
388
|
if (importers && importers.size > 0) {
|
|
@@ -396,13 +395,8 @@ async function detectCli() {
|
|
|
396
395
|
}
|
|
397
396
|
} else if (stat.isFile()) {
|
|
398
397
|
if (shouldIgnoreDetectionFile(resolved, process.cwd(), detectionConfig)) continue;
|
|
399
|
-
const ext = path.extname(resolved).toLowerCase();
|
|
400
398
|
const fileOptions = scanOptionsFor(resolved);
|
|
401
|
-
|
|
402
|
-
allFindings.push(...await detectHtml(resolved, fileOptions));
|
|
403
|
-
} else {
|
|
404
|
-
allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, fileOptions));
|
|
405
|
-
}
|
|
399
|
+
allFindings.push(...await detectLocalFile(resolved, fileOptions));
|
|
406
400
|
}
|
|
407
401
|
}
|
|
408
402
|
} finally {
|