@bastani/atomic 0.9.0-alpha.3 → 0.9.0

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +7 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +8 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +17 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +8 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +50 -0
  13. package/dist/builtin/workflows/README.md +12 -12
  14. package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -0
  15. package/dist/builtin/workflows/builtin/goal-runner.ts +96 -1
  16. package/dist/builtin/workflows/builtin/goal-types.ts +2 -0
  17. package/dist/builtin/workflows/builtin/goal.d.ts +3 -0
  18. package/dist/builtin/workflows/builtin/goal.ts +12 -1
  19. package/dist/builtin/workflows/builtin/index.d.ts +8 -8
  20. package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +359 -0
  21. package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +254 -352
  22. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +256 -414
  23. package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +272 -0
  24. package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +58 -68
  25. package/dist/builtin/workflows/builtin/open-claude-design.d.ts +5 -9
  26. package/dist/builtin/workflows/builtin/open-claude-design.ts +14 -26
  27. package/dist/builtin/workflows/package.json +1 -1
  28. package/dist/builtin/workflows/skills/impeccable/SKILL.md +14 -23
  29. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +2 -2
  30. package/dist/builtin/workflows/skills/impeccable/reference/live.md +25 -4
  31. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
  32. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +724 -29
  33. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +1 -1
  34. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +219 -7
  35. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +57 -11
  36. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +750 -0
  37. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +648 -53
  38. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
  39. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
  40. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +44 -11
  41. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
  42. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
  43. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +1 -1
  44. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +401 -46
  46. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +6 -6
  48. package/dist/builtin/workflows/skills/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
  49. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
  50. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
  51. package/dist/builtin/workflows/skills/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +2 -2
  52. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-args.mjs +42 -0
  53. package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
  54. package/dist/builtin/workflows/skills/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +1 -0
  55. package/dist/builtin/workflows/skills/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
  56. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
  57. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  58. package/dist/builtin/workflows/skills/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +21 -3
  60. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +835 -0
  61. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
  62. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +180 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +185 -60
  65. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +146 -0
  66. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +3369 -1026
  67. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
  68. package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +2 -2
  69. package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
  70. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +133 -9
  71. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +42 -2
  72. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +4 -4
  73. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +21 -15
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +1 -1
  75. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +205 -1269
  76. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +2 -2
  77. package/dist/builtin/workflows/skills/impeccable/scripts/live-target.mjs +30 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +69 -26
  79. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +73 -22
  80. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  81. package/dist/core/atomic-guide-command.js +5 -5
  82. package/dist/core/atomic-guide-command.js.map +1 -1
  83. package/docs/index.md +2 -2
  84. package/docs/quickstart.md +9 -9
  85. package/docs/workflows.md +42 -23
  86. package/package.json +2 -2
  87. package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
  88. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +0 -126
  89. /package/dist/builtin/workflows/skills/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
@@ -28,7 +28,7 @@
28
28
  import fs from 'node:fs';
29
29
  import path from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
- import { getCritiqueDir } from './impeccable-paths.mjs';
31
+ import { getCritiqueDir } from './lib/impeccable-paths.mjs';
32
32
 
33
33
  const SLUG_MAX = 50;
34
34
 
@@ -660,6 +660,7 @@ if (IS_BROWSER) {
660
660
  if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
661
661
  if (el.closest('[id^="impeccable-live-"]')) continue;
662
662
  if (el === document.body || el === document.documentElement) continue;
663
+ if (!isRenderedForBrowserRule(el)) continue;
663
664
 
664
665
  const tag = el.tagName.toLowerCase();
665
666
  const style = getComputedStyle(el);
@@ -1091,6 +1092,7 @@ if (IS_BROWSER) {
1091
1092
  return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'stale selector' };
1092
1093
  }
1093
1094
  if (!el) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'missing element' };
1095
+ if (!isRenderedForBrowserRule(el)) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'hidden element' };
1094
1096
 
1095
1097
  const blockingReason = (candidate.reasons || []).find(reason =>
1096
1098
  reason === 'background-clip text' ||
@@ -1222,6 +1224,7 @@ if (IS_BROWSER) {
1222
1224
  category: ap ? ap.category : 'quality',
1223
1225
  severity: ap?.severity || 'warning',
1224
1226
  detail: f.detail || f.snippet,
1227
+ ignoreValue: f.ignoreValue || f.value || '',
1225
1228
  name: ap ? ap.name : (f.type || f.id),
1226
1229
  description: ap ? ap.description : '',
1227
1230
  };
@@ -1258,10 +1261,203 @@ if (IS_BROWSER) {
1258
1261
  return [...groupMap.entries()].map(([el, findings]) => ({ el, findings }));
1259
1262
  }
1260
1263
 
1264
+ const DESIGN_COLOR_TOLERANCE = 6;
1265
+ const DESIGN_RADIUS_TOLERANCE_PX = 0.5;
1266
+ const DESIGN_SKIP_TAGS = new Set(['head', 'title', 'meta', 'link', 'style', 'script', 'noscript', 'template', 'source']);
1267
+
1268
+ function normalizeBrowserFontName(value) {
1269
+ return String(value || '')
1270
+ .trim()
1271
+ .replace(/^["']|["']$/g, '')
1272
+ .replace(/\+/g, ' ')
1273
+ .replace(/\s+/g, ' ')
1274
+ .toLowerCase();
1275
+ }
1276
+
1277
+ function browserPrimaryFont(stack) {
1278
+ if (!stack || /var\(/i.test(stack)) return '';
1279
+ return String(stack)
1280
+ .split(',')
1281
+ .map(normalizeBrowserFontName)
1282
+ .find(font => font && !GENERIC_FONTS.has(font)) || '';
1283
+ }
1284
+
1285
+ function browserDesignSystemConfig() {
1286
+ const raw = window.__IMPECCABLE_CONFIG__?.designSystem;
1287
+ if (!raw?.present) return null;
1288
+ const allowedFonts = new Set((raw.allowedFonts || []).map(normalizeBrowserFontName).filter(Boolean));
1289
+ const allowedColors = (raw.allowedColors || [])
1290
+ .filter(color => color && Number.isFinite(color.r) && Number.isFinite(color.g) && Number.isFinite(color.b))
1291
+ .map(color => ({ r: color.r, g: color.g, b: color.b }));
1292
+ const allowedRadii = (raw.allowedRadii || [])
1293
+ .map(Number)
1294
+ .filter(px => Number.isFinite(px));
1295
+ return {
1296
+ present: true,
1297
+ hasFonts: raw.hasFonts === true && allowedFonts.size > 0,
1298
+ allowedFonts,
1299
+ hasColors: raw.hasColors === true && allowedColors.length > 0,
1300
+ allowedColors,
1301
+ hasRadii: raw.hasRadii === true && allowedRadii.length > 0,
1302
+ allowedRadii,
1303
+ hasPillRadius: raw.hasPillRadius === true,
1304
+ };
1305
+ }
1306
+
1307
+ function browserColorsClose(a, b) {
1308
+ if (!a || !b) return false;
1309
+ return Math.max(
1310
+ Math.abs(a.r - b.r),
1311
+ Math.abs(a.g - b.g),
1312
+ Math.abs(a.b - b.b),
1313
+ ) <= DESIGN_COLOR_TOLERANCE;
1314
+ }
1315
+
1316
+ function isBrowserDesignColorAllowed(raw, designSystem) {
1317
+ if (!designSystem?.hasColors) return true;
1318
+ const text = String(raw || '').trim().toLowerCase();
1319
+ if (!text || text === 'transparent' || text === 'currentcolor' || text === 'inherit' || text === 'initial') return true;
1320
+ if (text.includes('var(')) return true;
1321
+ const parsed = parseAnyColor(text);
1322
+ if (!parsed) return true;
1323
+ if ((parsed.a ?? 1) <= 0.05) return true;
1324
+ return designSystem.allowedColors.some(color => browserColorsClose(parsed, color));
1325
+ }
1326
+
1327
+ function isBrowserTransparentCss(value) {
1328
+ const text = String(value || '').trim().toLowerCase();
1329
+ if (!text || text === 'transparent') return true;
1330
+ const parsed = parseAnyColor(text);
1331
+ return parsed ? (parsed.a ?? 1) <= 0.05 : false;
1332
+ }
1333
+
1334
+ function isBrowserDesignRadiusAllowed(raw, designSystem) {
1335
+ if (!designSystem?.hasRadii) return true;
1336
+ const text = String(raw || '').trim().toLowerCase();
1337
+ if (!text || text === '0' || text === 'none' || text === 'initial' || text === 'inherit') return true;
1338
+ if (text.includes('var(') || text.includes('%')) return true;
1339
+ const px = resolveLengthPx(text, 16);
1340
+ if (px == null || !Number.isFinite(px) || px <= DESIGN_RADIUS_TOLERANCE_PX) return true;
1341
+ if (designSystem.hasPillRadius && px >= 99) return true;
1342
+ return designSystem.allowedRadii.some(allowed => Math.abs(allowed - px) <= DESIGN_RADIUS_TOLERANCE_PX);
1343
+ }
1344
+
1345
+ function browserRadiusTokens(value) {
1346
+ return String(value || '')
1347
+ .replace(/\s*\/\s*/g, ' ')
1348
+ .split(/\s+/)
1349
+ .map(token => token.trim())
1350
+ .filter(Boolean);
1351
+ }
1352
+
1353
+ function browserHasDirectText(el) {
1354
+ return [...(el.childNodes || [])].some(node => node.nodeType === 3 && node.textContent.trim().length > 0);
1355
+ }
1356
+
1357
+ function browserSampleText(el) {
1358
+ const text = String(el.textContent || '').replace(/\s+/g, ' ').trim();
1359
+ return text ? ` "${text.slice(0, 40)}"` : '';
1360
+ }
1361
+
1362
+ function shouldSkipDesignElement(el) {
1363
+ const tag = el.tagName?.toLowerCase?.() || '';
1364
+ return DESIGN_SKIP_TAGS.has(tag) || isElementHidden(el);
1365
+ }
1366
+
1367
+ function checkElementDesignSystemDOM(el, designSystem, seen) {
1368
+ if (!designSystem?.present || shouldSkipDesignElement(el)) return [];
1369
+ const findings = [];
1370
+ const tag = el.tagName?.toLowerCase?.() || 'unknown';
1371
+ const style = getComputedStyle(el);
1372
+
1373
+ if (designSystem.hasFonts && browserHasDirectText(el)) {
1374
+ const font = browserPrimaryFont(style.fontFamily || '');
1375
+ if (font && !designSystem.allowedFonts.has(font) && !seen.fonts.has(font)) {
1376
+ seen.fonts.add(font);
1377
+ findings.push({
1378
+ type: 'design-system-font',
1379
+ detail: `${tag}${browserSampleText(el)} uses ${font}; not declared in DESIGN.md typography`,
1380
+ ignoreValue: font,
1381
+ });
1382
+ }
1383
+ }
1384
+
1385
+ if (designSystem.hasColors) {
1386
+ const colorChecks = [];
1387
+ if (browserHasDirectText(el)) colorChecks.push(['text color', style.color]);
1388
+ if (!isBrowserTransparentCss(style.backgroundColor)) colorChecks.push(['background', style.backgroundColor]);
1389
+ for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
1390
+ if ((parseFloat(style[`border${side}Width`]) || 0) > 0) {
1391
+ colorChecks.push([`border-${side.toLowerCase()}`, style[`border${side}Color`]]);
1392
+ }
1393
+ }
1394
+ if ((parseFloat(style.outlineWidth) || 0) > 0) colorChecks.push(['outline', style.outlineColor]);
1395
+
1396
+ for (const [kind, raw] of colorChecks) {
1397
+ const label = String(raw || '').trim().replace(/\s+/g, ' ');
1398
+ if (isBrowserDesignColorAllowed(label, designSystem)) continue;
1399
+ const key = `${kind}:${label}`;
1400
+ if (seen.colors.has(key)) continue;
1401
+ seen.colors.add(key);
1402
+ findings.push({
1403
+ type: 'design-system-color',
1404
+ detail: `${kind} ${label} on ${tag}${browserSampleText(el)} is outside DESIGN.md colors`,
1405
+ ignoreValue: label,
1406
+ });
1407
+ }
1408
+ }
1409
+
1410
+ if (designSystem.hasRadii) {
1411
+ for (const token of browserRadiusTokens(style.borderRadius || '')) {
1412
+ if (isBrowserDesignRadiusAllowed(token, designSystem)) continue;
1413
+ if (seen.radii.has(token)) continue;
1414
+ seen.radii.add(token);
1415
+ findings.push({
1416
+ type: 'design-system-radius',
1417
+ detail: `border-radius ${token} on ${tag}${browserSampleText(el)} is outside the DESIGN.md rounded scale`,
1418
+ ignoreValue: token,
1419
+ });
1420
+ }
1421
+ }
1422
+
1423
+ return findings;
1424
+ }
1425
+
1426
+ function decodeBrowserGoogleFamily(value) {
1427
+ const family = String(value || '').split(':')[0].replace(/\+/g, ' ');
1428
+ try {
1429
+ return decodeURIComponent(family);
1430
+ } catch {
1431
+ return family;
1432
+ }
1433
+ }
1434
+
1435
+ function checkBrowserDesignSystemSources(designSystem, seen) {
1436
+ if (!designSystem?.hasFonts) return [];
1437
+ const findings = [];
1438
+ for (const link of document.querySelectorAll('link[href*="fonts.googleapis.com/css"]')) {
1439
+ const href = link.getAttribute('href') || '';
1440
+ for (const match of href.matchAll(/[?&]family=([^&]+)/g)) {
1441
+ const display = decodeBrowserGoogleFamily(match[1]);
1442
+ const font = normalizeBrowserFontName(display);
1443
+ if (!font || designSystem.allowedFonts.has(font) || seen.fonts.has(font)) continue;
1444
+ seen.fonts.add(font);
1445
+ findings.push({
1446
+ type: 'design-system-font',
1447
+ detail: `Google Fonts: ${display} is not declared in DESIGN.md typography`,
1448
+ ignoreValue: display,
1449
+ });
1450
+ }
1451
+ }
1452
+ return findings;
1453
+ }
1454
+
1261
1455
  function collectBrowserFindings() {
1262
1456
  const groupMap = new Map();
1263
1457
  const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : [];
1264
1458
  const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
1459
+ const designSystem = browserDesignSystemConfig();
1460
+ const designSeen = { fonts: new Set(), colors: new Set(), radii: new Set() };
1265
1461
  // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a
1266
1462
  // real browser env (detector page, live overlay, extension) running every
1267
1463
  // check is free, so we always surface them; the gating is purely a CLI
@@ -1292,6 +1488,7 @@ if (IS_BROWSER) {
1292
1488
  ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1293
1489
  ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1294
1490
  ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
1491
+ ...checkElementDesignSystemDOM(el, designSystem, designSeen),
1295
1492
  ].filter(f => _ruleOk(f.type));
1296
1493
 
1297
1494
  addBrowserFindings(groupMap, el, findings);
@@ -1308,6 +1505,13 @@ if (IS_BROWSER) {
1308
1505
 
1309
1506
  const pageLevelFindings = [];
1310
1507
 
1508
+ const designSourceFindings = checkBrowserDesignSystemSources(designSystem, designSeen)
1509
+ .filter(f => _ruleOk(f.type));
1510
+ if (designSourceFindings.length > 0) {
1511
+ pageLevelFindings.push(...designSourceFindings);
1512
+ addBrowserFindings(groupMap, document.body, designSourceFindings);
1513
+ }
1514
+
1311
1515
  const typoFindings = checkTypography().filter(f => _ruleOk(f.type));
1312
1516
  if (typoFindings.length > 0) {
1313
1517
  pageLevelFindings.push(...typoFindings);
@@ -1437,13 +1641,20 @@ if (IS_BROWSER) {
1437
1641
  return true;
1438
1642
  }
1439
1643
 
1440
- function postSerializedFindings(groupMap) {
1644
+ function scanResultMeta(options = {}) {
1645
+ const scanId = options.scanId;
1646
+ if (typeof scanId !== 'string' && typeof scanId !== 'number') return {};
1647
+ return { scanId: String(scanId) };
1648
+ }
1649
+
1650
+ function postSerializedFindings(groupMap, options = {}) {
1441
1651
  if (!EXTENSION_MODE) return;
1442
1652
  const allFindings = browserFindingsFromMap(groupMap);
1443
1653
  window.postMessage({
1444
1654
  source: 'impeccable-results',
1445
1655
  findings: serializeFindings(allFindings),
1446
1656
  count: allFindings.length,
1657
+ ...scanResultMeta(options),
1447
1658
  }, '*');
1448
1659
  }
1449
1660
 
@@ -1497,7 +1708,7 @@ if (IS_BROWSER) {
1497
1708
  rememberVisualContrastAnalysis(result);
1498
1709
  const added = addVisualContrastResult(groupMap, result, { decorate: true });
1499
1710
  if (added) {
1500
- postSerializedFindings(groupMap);
1711
+ postSerializedFindings(groupMap, options);
1501
1712
  window.dispatchEvent(new CustomEvent('impeccable-visual-contrast-resolved', {
1502
1713
  detail: {
1503
1714
  selector: result.selector,
@@ -1565,7 +1776,7 @@ if (IS_BROWSER) {
1565
1776
  overlayIndex = 0;
1566
1777
  }
1567
1778
 
1568
- function renderBrowserFindings(collected) {
1779
+ function renderBrowserFindings(collected, options = {}) {
1569
1780
  const { allFindings, pageLevelFindings } = collected;
1570
1781
 
1571
1782
  for (const { el, findings } of allFindings) {
@@ -1585,6 +1796,7 @@ if (IS_BROWSER) {
1585
1796
  source: 'impeccable-results',
1586
1797
  findings: serializeFindings(allFindings),
1587
1798
  count: allFindings.length,
1799
+ ...scanResultMeta(options),
1588
1800
  }, '*');
1589
1801
  }
1590
1802
 
@@ -1599,11 +1811,11 @@ if (IS_BROWSER) {
1599
1811
  clearOverlays();
1600
1812
  const generation = scanGeneration;
1601
1813
  const collected = collectBrowserFindings();
1602
- const allFindings = renderBrowserFindings(collected);
1814
+ const allFindings = renderBrowserFindings(collected, options);
1603
1815
  if (shouldRunVisualContrast(options)) {
1604
1816
  addVisualContrastFindings(collected.groupMap, options, { decorate: true, generation })
1605
1817
  .then(() => {
1606
- if (generation === scanGeneration) postSerializedFindings(collected.groupMap);
1818
+ if (generation === scanGeneration) postSerializedFindings(collected.groupMap, options);
1607
1819
  })
1608
1820
  .catch(err => {
1609
1821
  reportVisualContrastError(err);
@@ -1618,10 +1830,10 @@ if (IS_BROWSER) {
1618
1830
  if (shouldRunVisualContrast(options)) {
1619
1831
  const collected = await collectBrowserFindingsAsync(options, { generation, scheduleLazy: true });
1620
1832
  if (generation !== scanGeneration) return [];
1621
- return renderBrowserFindings(collected);
1833
+ return renderBrowserFindings(collected, options);
1622
1834
  }
1623
1835
  lastVisualContrastAnalyses = [];
1624
- return renderBrowserFindings(collectBrowserFindings());
1836
+ return renderBrowserFindings(collectBrowserFindings(), options);
1625
1837
  };
1626
1838
 
1627
1839
  const detect = function(options = {}) {
@@ -1,9 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ import { loadDesignSystemForCwd } from '../design-system.mjs';
4
5
  import { createBrowserDetector, detectUrl } from '../engines/browser/detect-url.mjs';
5
6
  import { detectHtml } from '../engines/static-html/detect-html.mjs';
6
7
  import { detectText } from '../engines/regex/detect-text.mjs';
8
+ import {
9
+ filterDetectionFindings,
10
+ readDetectionConfig,
11
+ shouldIgnoreDetectionFile,
12
+ } from '../../lib/impeccable-config.mjs';
7
13
  import {
8
14
  HTML_EXTENSIONS,
9
15
  buildImportGraph,
@@ -16,6 +22,10 @@ import {
16
22
  // Output formatting
17
23
  // ---------------------------------------------------------------------------
18
24
 
25
+ function formatFindingSummary(count) {
26
+ return `${count} anti-pattern${count === 1 ? '' : 's'} found.`;
27
+ }
28
+
19
29
  function formatFindings(findings, jsonMode) {
20
30
  if (jsonMode) return JSON.stringify(findings, null, 2);
21
31
 
@@ -33,7 +43,7 @@ function formatFindings(findings, jsonMode) {
33
43
  out.push(` → ${item.description}`);
34
44
  }
35
45
  }
36
- out.push(`\n${findings.length} anti-pattern${findings.length === 1 ? '' : 's'} found.`);
46
+ out.push(`\n${formatFindingSummary(findings.length)}`);
37
47
  return out.join('\n');
38
48
  }
39
49
 
@@ -79,10 +89,28 @@ function printUsage() {
79
89
  Scan files or URLs for UI anti-patterns and design quality issues.
80
90
 
81
91
  Options:
82
- --json Output results as JSON
83
- --gpt Also report GPT-specific provider tells (off by default)
84
- --gemini Also report Gemini-specific provider tells (off by default)
85
- --help Show this help message
92
+ --json Output results as JSON
93
+ --quiet In text mode, only print the final findings count
94
+ --gpt Also report GPT-specific provider tells (off by default)
95
+ --gemini Also report Gemini-specific provider tells (off by default)
96
+ --no-config Do not apply project config, detector ignores, inline
97
+ ignore comments, or DESIGN.md
98
+ --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments
99
+ --no-design-system Do not load local DESIGN.md / .impeccable/design.json context
100
+ --help Show this help message
101
+
102
+ Project config:
103
+ Respects .impeccable/config.json and .impeccable/config.local.json detector
104
+ settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
105
+ and detector.designSystem.enabled.
106
+
107
+ Inline ignores:
108
+ In-file comments waive a finding where it lives and travel with the file:
109
+ <!-- impeccable-disable overused-font -- exported brand doc -->
110
+ .brand { font-family: Inter } /* impeccable-disable-line overused-font */
111
+ // impeccable-disable-next-line bounce-easing: intentional bounce
112
+ impeccable-disable applies to the whole file; -line / -next-line are scoped.
113
+ List one or more rule ids (comma-separated), or omit them / use * for all.
86
114
 
87
115
  Detection modes:
88
116
  HTML files Static HTML/CSS analysis (default, catches linked CSS)
@@ -93,7 +121,8 @@ Examples:
93
121
  impeccable detect src/
94
122
  impeccable detect index.html
95
123
  impeccable detect https://example.com
96
- impeccable detect --json .`);
124
+ impeccable detect --json .
125
+ impeccable detect --no-config src/`);
97
126
  }
98
127
 
99
128
  async function detectCli() {
@@ -104,6 +133,7 @@ async function detectCli() {
104
133
  });
105
134
  if (args[0] === 'detect') args = args.slice(1);
106
135
  const jsonMode = args.includes('--json');
136
+ const quietMode = args.includes('--quiet');
107
137
  const helpMode = args.includes('--help');
108
138
  // --fast (regex-only) is deprecated: since the jsdom removal, the static
109
139
  // HTML/CSS analysis is fast and covers every rule, so the regex-only path
@@ -114,10 +144,21 @@ async function detectCli() {
114
144
  'Note: --fast is deprecated and ignored. The full scan is fast now and runs every rule.\n',
115
145
  );
116
146
  }
147
+ const configEnabled = !args.includes('--no-config');
148
+ const detectionConfig = configEnabled
149
+ ? readDetectionConfig(process.cwd())
150
+ : { ignoreRules: [], ignoreFiles: [], ignoreValues: [] };
117
151
  const providers = [];
118
152
  if (args.includes('--gpt')) providers.push('gpt');
119
153
  if (args.includes('--gemini')) providers.push('gemini');
120
- const scanOptions = { providers };
154
+ const designSystemEnabled = configEnabled && !args.includes('--no-design-system') && detectionConfig.designSystem?.enabled !== false;
155
+ const designSystem = designSystemEnabled ? loadDesignSystemForCwd(process.cwd()) : null;
156
+ // Inline `impeccable-disable*` waivers are part of the scanned file, so they
157
+ // apply by default. `--no-config` (raw scan) and the dedicated
158
+ // `--no-inline-ignores` both turn them off.
159
+ const inlineIgnoresEnabled = configEnabled && !args.includes('--no-inline-ignores');
160
+ const scanOptions = { providers, inlineIgnores: inlineIgnoresEnabled };
161
+ if (designSystem) scanOptions.designSystem = designSystem;
121
162
  const targets = args.filter(a => !a.startsWith('--'));
122
163
 
123
164
  if (helpMode) { printUsage(); process.exit(0); }
@@ -149,8 +190,8 @@ async function detectCli() {
149
190
  catch { process.stderr.write(`Warning: cannot access ${target}\n`); continue; }
150
191
 
151
192
  if (stat.isDirectory()) {
152
- // Check for framework dev server config (skip in JSON mode to avoid polluting output)
153
- if (!jsonMode) {
193
+ // Check for framework dev server config (skip in JSON/quiet modes to avoid polluting output)
194
+ if (!jsonMode && !quietMode) {
154
195
  const fwConfig = detectFrameworkConfig(resolved);
155
196
  if (fwConfig) {
156
197
  const probe = await isPortListening(fwConfig.port, fwConfig.fingerprint);
@@ -175,11 +216,12 @@ async function detectCli() {
175
216
  }
176
217
  }
177
218
 
178
- const files = walkDir(resolved);
219
+ const files = walkDir(resolved)
220
+ .filter(file => !shouldIgnoreDetectionFile(file, process.cwd(), detectionConfig));
179
221
  const htmlCount = files.filter(f => HTML_EXTENSIONS.has(path.extname(f).toLowerCase())).length;
180
222
 
181
223
  // Warn and confirm if scanning many files (static HTML/CSS processes each HTML file)
182
- if (files.length > 50 && process.stdin.isTTY && !jsonMode) {
224
+ if (files.length > 50 && process.stdin.isTTY && !jsonMode && !quietMode) {
183
225
  process.stderr.write(
184
226
  `\nFound ${files.length} files (${htmlCount} HTML) in ${target}.\n` +
185
227
  `Scanning may take a while${htmlCount > 10 ? ' (static HTML/CSS processes each HTML file individually)' : ''}.\n` +
@@ -219,6 +261,7 @@ async function detectCli() {
219
261
  allFindings.push(...fileFindings);
220
262
  }
221
263
  } else if (stat.isFile()) {
264
+ if (shouldIgnoreDetectionFile(resolved, process.cwd(), detectionConfig)) continue;
222
265
  const ext = path.extname(resolved).toLowerCase();
223
266
  if (HTML_EXTENSIONS.has(ext)) {
224
267
  allFindings.push(...await detectHtml(resolved, scanOptions));
@@ -232,8 +275,11 @@ async function detectCli() {
232
275
  }
233
276
  }
234
277
 
278
+ allFindings = filterDetectionFindings(allFindings, detectionConfig);
279
+
235
280
  if (allFindings.length > 0) {
236
281
  if (jsonMode) process.stdout.write(formatFindings(allFindings, true) + '\n');
282
+ else if (quietMode) process.stderr.write(formatFindingSummary(allFindings.length) + '\n');
237
283
  else process.stderr.write(formatFindings(allFindings, false) + '\n');
238
284
  process.exit(2);
239
285
  }