@bastani/atomic 0.9.0-alpha.3 → 0.9.0-alpha.4
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/CHANGELOG.md +15 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +17 -0
- package/dist/builtin/workflows/README.md +12 -12
- package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -0
- package/dist/builtin/workflows/builtin/goal-runner.ts +96 -1
- package/dist/builtin/workflows/builtin/goal-types.ts +2 -0
- package/dist/builtin/workflows/builtin/goal.d.ts +3 -0
- package/dist/builtin/workflows/builtin/goal.ts +12 -1
- package/dist/builtin/workflows/builtin/index.d.ts +8 -8
- package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +359 -0
- package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +254 -352
- package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +256 -414
- package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +272 -0
- package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +58 -68
- package/dist/builtin/workflows/builtin/open-claude-design.d.ts +5 -9
- package/dist/builtin/workflows/builtin/open-claude-design.ts +14 -26
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +14 -23
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +25 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +724 -29
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +219 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +57 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +750 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +648 -53
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +44 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +401 -46
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +6 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-args.mjs +42 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +21 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +835 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +180 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +185 -60
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +3369 -1026
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +133 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +42 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +4 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +21 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +205 -1269
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-target.mjs +30 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +69 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +73 -22
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +5 -5
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/docs/index.md +2 -2
- package/docs/quickstart.md +9 -9
- package/docs/workflows.md +42 -23
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +0 -126
- /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
|
|
package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs
CHANGED
|
@@ -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
|
|
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}
|
|
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
|
|
83
|
-
--
|
|
84
|
-
--
|
|
85
|
-
--
|
|
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
|
|
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
|
|
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
|
}
|