Detector architecture v2: static engine, benchmarks, lab, and visual contrast (#156)

* Add detector benchmark lab and visual contrast fallback

* Expand visual contrast fixture coverage

* Add browser visual contrast fallback

* Show visual contrast overlays in detector lab

* Fix detector lab short viewport layout

* Fix detector lab visual overlays

* Add visual contrast to browser scan overlays

* Avoid browser scroll jumps during visual contrast scans

* Resolve visual contrast lazily on scroll

* Refresh detector lab visual counts lazily

* Update pnpm lockfile for static parser deps

* Address Bugbot detector API comments

* Report extension visual contrast errors

* Refactor detector into engine modules

* Address Bugbot detector comments

* Fix latest Bugbot detector notes

* Fix visual contrast fixture labels

* Refine detector lab fixtures

* Fix stale detector overlay references

* Fix detector lab fixture URLs

* Fix typography lab fixture highlights

* Fix typography lab page-level signal

* Fix visual overlay lifecycle cleanup

* Remove dead spotlight timer cleanup

* Make browser async APIs reject consistently
This commit is contained in:
Paul Bakaus
2026-05-17 19:49:38 -07:00
committed by GitHub
parent 4af581e23f
commit e1d3ea0b6f
46 changed files with 11480 additions and 4915 deletions
+583
View File
@@ -0,0 +1,583 @@
#!/usr/bin/env node
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
createBrowserDetector,
createDetectorProfile,
detectHtml,
detectText,
detectUrl,
summarizeDetectorProfile,
walkDir,
} from '../cli/engine/detect-antipatterns.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const FIXTURES = path.join(ROOT, 'tests', 'fixtures', 'antipatterns');
const BROWSER_FIXTURES = [
'cramped-padding.html',
'quality.html',
'body-text-viewport-edge.html',
];
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
};
function parseArgs(argv) {
const args = {
browser: false,
json: false,
out: null,
quick: false,
};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--browser') args.browser = true;
else if (arg === '--json') args.json = true;
else if (arg === '--quick') args.quick = true;
else if (arg === '--out') args.out = argv[++i] || null;
else if (arg.startsWith('--out=')) args.out = arg.slice('--out='.length);
else if (arg === '--help') {
printUsage();
process.exit(0);
}
}
return args;
}
function printUsage() {
console.log(`Usage: node scripts/benchmark-detector.mjs [options]
Options:
--quick Run a small smoke benchmark subset
--browser Include browser-backed URL benchmarks
--json Print the benchmark report as JSON
--out FILE Write the benchmark report JSON to FILE
--help Show this help message`);
}
function nowMs() {
return typeof performance !== 'undefined' && performance.now
? performance.now()
: Date.now();
}
function roundMs(value) {
return Number(value.toFixed(3));
}
function isHtml(filePath) {
const ext = path.extname(filePath).toLowerCase();
return ext === '.html' || ext === '.htm';
}
function rel(filePath) {
return path.relative(ROOT, filePath);
}
function addEvent(profile, event) {
profile.events.push({
engine: event.engine || 'unknown',
phase: event.phase || 'unknown',
ruleId: event.ruleId || 'unknown',
target: event.target || '',
ms: Number.isFinite(event.ms) ? event.ms : 0,
findings: Number.isFinite(event.findings) ? event.findings : 0,
});
}
async function measureCase({ name, engine, mode, target, run }) {
const profile = createDetectorProfile();
const started = nowMs();
try {
const result = await run(profile);
const findings = Array.isArray(result)
? result.length
: (Number.isFinite(result?.findings) ? result.findings : 0);
return {
name,
engine,
mode,
target,
status: 'ok',
totalMs: roundMs(nowMs() - started),
findings,
profile: summarizeDetectorProfile(profile),
events: profile.events,
};
} catch (err) {
return {
name,
engine,
mode,
target,
status: 'failed',
totalMs: roundMs(nowMs() - started),
findings: 0,
error: err?.message || String(err),
profile: summarizeDetectorProfile(profile),
events: profile.events,
};
}
}
function skippedCase({ name, engine, mode, target, reason }) {
return {
name,
engine,
mode,
target,
status: 'skipped',
totalMs: 0,
findings: 0,
skipReason: reason,
profile: [],
events: [],
};
}
async function scanDirectory(files, fastMode, profile) {
const findings = [];
for (const file of files) {
if (!fastMode && isHtml(file)) {
findings.push(...await detectHtml(file, { profile }));
} else {
const content = fs.readFileSync(file, 'utf-8');
findings.push(...detectText(content, file, { profile }));
}
}
return findings;
}
function selectQuickFiles(files, predicate, preferredNames) {
const preferred = preferredNames
.map(name => files.find(file => path.basename(file) === name))
.filter(Boolean);
const fallback = files.filter(predicate).slice(0, preferredNames.length || 2);
return preferred.length ? preferred : fallback;
}
async function runFileBenchmarks(args) {
const files = walkDir(FIXTURES).sort();
const htmlFiles = files.filter(isHtml);
const textFiles = files.filter(file => !isHtml(file));
const selectedText = args.quick
? textFiles.slice(0, 2)
: textFiles;
const selectedHtml = args.quick
? selectQuickFiles(htmlFiles, isHtml, ['color.html', 'quality.html'])
: htmlFiles;
const directoryFiles = args.quick
? [...selectedHtml, ...selectedText].sort()
: files;
const cases = [];
for (const file of selectedText) {
cases.push(await measureCase({
name: `detectText:${rel(file)}`,
engine: 'regex',
mode: 'file',
target: rel(file),
run: (profile) => detectText(fs.readFileSync(file, 'utf-8'), file, { profile }),
}));
}
for (const file of selectedHtml) {
cases.push(await measureCase({
name: `detectHtml:${rel(file)}`,
engine: 'static-html',
mode: 'file',
target: rel(file),
run: (profile) => detectHtml(file, { profile }),
}));
}
cases.push(await measureCase({
name: args.quick ? 'directory-default:quick-fixtures' : 'directory-default:all-fixtures',
engine: 'mixed',
mode: 'directory-default',
target: rel(FIXTURES),
run: (profile) => scanDirectory(directoryFiles, false, profile),
}));
cases.push(await measureCase({
name: args.quick ? 'directory-fast:quick-fixtures' : 'directory-fast:all-fixtures',
engine: 'regex',
mode: 'directory-fast',
target: rel(FIXTURES),
run: (profile) => scanDirectory(directoryFiles, true, profile),
}));
return cases;
}
function startFixtureServer() {
const server = http.createServer((req, res) => {
let filePath;
const urlPath = req.url?.split('?')[0] || '/';
if (urlPath.startsWith('/fixtures/')) {
filePath = path.join(ROOT, 'tests', urlPath);
} else if (urlPath === '/js/detect-antipatterns-browser.js') {
filePath = path.join(ROOT, 'cli', 'engine', 'detect-antipatterns-browser.js');
} else {
res.writeHead(404).end();
return;
}
try {
const body = fs.readFileSync(filePath);
const ext = path.extname(filePath).toLowerCase();
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(body);
} catch {
res.writeHead(404).end();
}
});
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
const address = server.address();
resolve({
server,
baseUrl: `http://127.0.0.1:${address.port}`,
});
});
});
}
async function closeServer(server) {
await new Promise(resolve => server.close(resolve));
}
async function runBrowserBenchmarks(args) {
let serverInfo;
try {
serverInfo = await startFixtureServer();
} catch (err) {
return [
skippedCase({
name: 'browser:fixtures',
engine: 'browser',
mode: 'browser',
target: 'localhost',
reason: `localhost fixture server unavailable: ${err?.message || err}`,
}),
];
}
const cases = [];
const browserFiles = args.quick ? ['quality.html'] : BROWSER_FIXTURES;
try {
for (const fileName of browserFiles) {
const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${fileName}`;
const fresh = await measureCase({
name: `detectUrl:fresh-load:${fileName}`,
engine: 'browser',
mode: 'fresh-load',
target: url,
run: (profile) => detectUrl(url, { profile, waitUntil: 'load', settleMs: 100 }),
});
if (fresh.status === 'failed' && /Could not find Chrome|Failed to launch|executable|spawn|puppeteer/i.test(fresh.error || '')) {
cases.push(skippedCase({
name: `detectUrl:fresh-load:${fileName}`,
engine: 'browser',
mode: 'fresh-load',
target: url,
reason: `Chromium unavailable: ${fresh.error}`,
}));
} else {
cases.push(fresh);
}
}
const visualContrastUrl = `${serverInfo.baseUrl}/fixtures/antipatterns/visual-contrast.html`;
cases.push(await measureCase({
name: 'detectUrl:visual-contrast',
engine: 'browser',
mode: 'visual-contrast',
target: visualContrastUrl,
run: (profile) => detectUrl(visualContrastUrl, {
profile,
waitUntil: 'load',
settleMs: 0,
visualContrast: true,
}),
}));
cases.push(await measureCase({
name: 'detectUrl:warm-load',
engine: 'browser',
mode: 'warm-load',
target: serverInfo.baseUrl,
run: async (profile) => {
const detector = await createBrowserDetector({ waitUntil: 'load', settleMs: 100 });
const findings = [];
try {
for (const fileName of browserFiles) {
const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${fileName}`;
findings.push(...await detector.detectUrl(url, { profile }));
}
} finally {
await detector.close();
}
return findings;
},
}));
cases.push(await measureCase({
name: 'detectUrl:warm-networkidle0',
engine: 'browser',
mode: 'warm-networkidle0',
target: serverInfo.baseUrl,
run: async (profile) => {
const detector = await createBrowserDetector({ waitUntil: 'load', settleMs: 100 });
const findings = [];
try {
for (const fileName of browserFiles) {
const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${fileName}`;
findings.push(...await detector.detectUrl(url, {
profile,
waitUntil: 'networkidle0',
settleMs: 0,
}));
}
} finally {
await detector.close();
}
return findings;
},
}));
let puppeteer;
try {
puppeteer = await import('puppeteer');
} catch (err) {
cases.push(skippedCase({
name: 'browser:pure-vs-overlay',
engine: 'browser',
mode: 'pure-vs-overlay',
target: serverInfo.baseUrl,
reason: `puppeteer unavailable: ${err?.message || err}`,
}));
return cases;
}
cases.push(await measureCase({
name: 'browser:pure-vs-overlay',
engine: 'browser',
mode: 'pure-vs-overlay',
target: serverInfo.baseUrl,
run: async (profile) => {
let browser;
const launchStarted = nowMs();
try {
browser = await puppeteer.default.launch({
headless: true,
args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
});
addEvent(profile, {
engine: 'browser',
phase: 'load',
ruleId: 'launch-browser-overlay-bench',
target: serverInfo.baseUrl,
ms: nowMs() - launchStarted,
});
} catch (err) {
throw new Error(`Chromium unavailable: ${err?.message || err}`);
}
let findings = [];
try {
const page = await browser.newPage();
const url = `${serverInfo.baseUrl}/fixtures/antipatterns/${browserFiles[0]}`;
const browserScript = fs.readFileSync(path.join(ROOT, 'cli', 'engine', 'detect-antipatterns-browser.js'), 'utf-8');
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, { waitUntil: 'load', timeout: 30000 });
await new Promise(resolve => setTimeout(resolve, 100));
await page.evaluate(() => { window.__IMPECCABLE_CONFIG__ = { autoScan: false }; });
await page.evaluate(browserScript);
const pureStarted = nowMs();
findings = await page.evaluate(() => {
const serialized = window.impeccableDetect({ decorate: false, serialize: true });
return serialized.flatMap(({ findings }) => findings.map(f => ({ id: f.type, snippet: f.detail })));
});
addEvent(profile, {
engine: 'browser',
phase: 'scan',
ruleId: 'pure-detect',
target: url,
ms: nowMs() - pureStarted,
findings: findings.length,
});
const overlayStarted = nowMs();
const overlayGroupCount = await page.evaluate(() => window.impeccableScan().length);
addEvent(profile, {
engine: 'browser',
phase: 'scan',
ruleId: 'overlay-scan',
target: url,
ms: nowMs() - overlayStarted,
findings: overlayGroupCount,
});
await page.close().catch(() => {});
} finally {
const closeStarted = nowMs();
await browser.close().catch(() => {});
addEvent(profile, {
engine: 'browser',
phase: 'load',
ruleId: 'close-browser-overlay-bench',
target: serverInfo.baseUrl,
ms: nowMs() - closeStarted,
});
}
return findings;
},
}));
} finally {
await closeServer(serverInfo.server);
}
return cases.map(testCase => {
if (testCase.engine === 'browser' && testCase.status === 'failed' && /Chromium unavailable|Failed to launch|Could not find Chrome|executable|spawn|puppeteer/i.test(testCase.error || '')) {
return skippedCase({
name: testCase.name,
engine: testCase.engine,
mode: testCase.mode,
target: testCase.target,
reason: testCase.error,
});
}
return testCase;
});
}
function aggregateEvents(cases) {
const profile = createDetectorProfile();
for (const testCase of cases) {
if (Array.isArray(testCase.events)) profile.events.push(...testCase.events);
}
return summarizeDetectorProfile(profile);
}
function makeReport(args, cases) {
const summary = aggregateEvents(cases);
return {
version: 1,
createdAt: new Date().toISOString(),
cwd: ROOT,
quick: args.quick,
browser: args.browser,
cases: cases.map(({ events, ...testCase }) => testCase),
summary,
};
}
function pad(value, width) {
const str = String(value);
if (str.length >= width) return str.slice(0, width);
return str + ' '.repeat(width - str.length);
}
function printRows(rows, columns) {
const header = columns.map(col => pad(col.label, col.width)).join(' ');
console.log(header);
console.log(columns.map(col => '-'.repeat(col.width)).join(' '));
for (const row of rows) {
console.log(columns.map(col => pad(row[col.key] ?? '', col.width)).join(' '));
}
}
function printConsoleReport(report) {
console.log(`Detector benchmark ${report.quick ? '(quick)' : '(full)'}`);
console.log(`Cases: ${report.cases.length}`);
const caseRows = report.cases.map(testCase => ({
status: testCase.status,
engine: testCase.engine,
mode: testCase.mode,
totalMs: testCase.totalMs,
findings: testCase.findings,
target: testCase.target,
}));
printRows(caseRows, [
{ key: 'status', label: 'Status', width: 8 },
{ key: 'engine', label: 'Engine', width: 12 },
{ key: 'mode', label: 'Mode', width: 20 },
{ key: 'totalMs', label: 'Total ms', width: 10 },
{ key: 'findings', label: 'Findings', width: 8 },
{ key: 'target', label: 'Target', width: 60 },
]);
const skipped = report.cases.filter(testCase => testCase.status === 'skipped');
for (const testCase of skipped) {
console.log(`Skipped ${testCase.name}: ${testCase.skipReason}`);
}
console.log('\nSlowest profile groups');
const slowRows = report.summary.slice(0, 20).map(item => ({
engine: item.engine,
phase: item.phase,
ruleId: item.ruleId,
calls: item.calls,
totalMs: item.totalMs,
avgMs: item.avgMs,
p95: item.p95,
findings: item.findings,
target: item.target,
}));
printRows(slowRows, [
{ key: 'engine', label: 'Engine', width: 12 },
{ key: 'phase', label: 'Phase', width: 14 },
{ key: 'ruleId', label: 'Rule', width: 28 },
{ key: 'calls', label: 'Calls', width: 8 },
{ key: 'totalMs', label: 'Total ms', width: 10 },
{ key: 'avgMs', label: 'Avg ms', width: 8 },
{ key: 'p95', label: 'P95', width: 8 },
{ key: 'findings', label: 'Finds', width: 7 },
{ key: 'target', label: 'Target', width: 45 },
]);
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const cases = [
...await runFileBenchmarks(args),
];
if (args.browser) {
cases.push(...await runBrowserBenchmarks(args));
}
const report = makeReport(args, cases);
const json = JSON.stringify(report, null, 2);
if (args.out) {
fs.writeFileSync(path.resolve(args.out), json + '\n');
}
if (args.json) {
process.stdout.write(json + '\n');
} else {
printConsoleReport(report);
if (args.out) console.log(`\nWrote JSON report to ${path.resolve(args.out)}`);
}
if (report.cases.some(testCase => testCase.status === 'failed')) {
process.exitCode = 1;
}
}
main().catch(err => {
console.error(err?.stack || err?.message || err);
process.exit(1);
});
+25 -10
View File
@@ -2,7 +2,7 @@
/**
* Generates cli/engine/detect-antipatterns-browser.js
* by stripping Node-specific sections from the universal source and wrapping in an IIFE.
* by concatenating the browser-safe detector modules and wrapping them in an IIFE.
*
* Run: node scripts/build-browser-detector.js
*/
@@ -14,24 +14,36 @@ import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const SOURCE = path.join(ROOT, 'cli/engine/detect-antipatterns.mjs');
const MODULES = [
'cli/engine/shared/constants.mjs',
'cli/engine/registry/antipatterns.mjs',
'cli/engine/shared/color.mjs',
'cli/engine/rules/checks.mjs',
'cli/engine/browser/injected/index.mjs',
];
const OUTPUT = path.join(ROOT, 'cli/engine/detect-antipatterns-browser.js');
const SITE_OUTPUT = path.join(ROOT, 'site/public/js/detect-antipatterns-browser.js');
let code = fs.readFileSync(SOURCE, 'utf-8');
function browserSafeModule(relPath) {
let code = fs.readFileSync(path.join(ROOT, relPath), 'utf-8');
if (relPath === 'cli/engine/registry/antipatterns.mjs') {
const match = code.match(/const ANTIPATTERNS = \[[\s\S]*?\n\];/);
if (!match) throw new Error('Could not extract browser antipattern registry');
code = match[0];
}
code = code.replace(/^import[\s\S]*?;\n/gm, '');
code = code.replace(/^export\s+\{[\s\S]*?^};\n?/gm, '');
return `// --- ${relPath} ---\n${code.trim()}\n`;
}
// Strip shebang
code = code.replace(/^#!.*\n/, '');
// Strip sections between @browser-strip-start / @browser-strip-end markers
code = code.replace(/^\/\/ @browser-strip-start\n[\s\S]*?^\/\/ @browser-strip-end\n?/gm, '');
// Set IS_BROWSER = true (dead-code eliminates Node paths)
code = code.replace(/^const IS_BROWSER = .*$/m, 'const IS_BROWSER = true;');
const code = MODULES.map(browserSafeModule).join('\n');
const output = `/**
* Anti-Pattern Browser Detector for Impeccable
* Copyright (c) 2026 Paul Bakaus
* SPDX-License-Identifier: Apache-2.0
*
* GENERATED -- do not edit. Source: detect-antipatterns.mjs
* GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs
* Rebuild: node scripts/build-browser-detector.js
*
* Usage: <script src="detect-antipatterns-browser.js"></script>
@@ -44,4 +56,7 @@ ${code}
`;
fs.writeFileSync(OUTPUT, output);
fs.mkdirSync(path.dirname(SITE_OUTPUT), { recursive: true });
fs.writeFileSync(SITE_OUTPUT, output);
console.log(`Generated ${path.relative(ROOT, OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
console.log(`Generated ${path.relative(ROOT, SITE_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
+32 -26
View File
@@ -13,32 +13,44 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const EXT_DIR = path.join(ROOT, 'extension');
const SOURCE = path.join(ROOT, 'cli/engine/detect-antipatterns.mjs');
const BROWSER_MODULES = [
'cli/engine/shared/constants.mjs',
'cli/engine/registry/antipatterns.mjs',
'cli/engine/shared/color.mjs',
'cli/engine/rules/checks.mjs',
'cli/engine/browser/injected/index.mjs',
];
const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js');
const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json');
let code = fs.readFileSync(SOURCE, 'utf-8');
function browserSafeModule(relPath) {
let code = fs.readFileSync(path.join(ROOT, relPath), 'utf-8');
if (relPath === 'cli/engine/registry/antipatterns.mjs') {
const match = code.match(/const ANTIPATTERNS = \[[\s\S]*?\n\];/);
if (!match) throw new Error('Could not extract browser antipattern registry');
code = match[0];
}
code = code.replace(/^import[\s\S]*?;\n/gm, '');
code = code.replace(/^export\s+\{[\s\S]*?^};\n?/gm, '');
return `// --- ${relPath} ---\n${code.trim()}\n`;
}
const code = BROWSER_MODULES.map(browserSafeModule).join('\n');
// --- 1. Build detector ---
// Strip shebang
code = code.replace(/^#!.*\n/, '');
// Strip sections between @browser-strip-start / @browser-strip-end markers
code = code.replace(/^\/\/ @browser-strip-start\n[\s\S]*?^\/\/ @browser-strip-end\n?/gm, '');
// Set IS_BROWSER = true (dead-code eliminates Node paths)
code = code.replace(/^const IS_BROWSER = .*$/m, 'const IS_BROWSER = true;');
const output = `/**
* Anti-Pattern Browser Detector for Impeccable (Extension Variant)
* Copyright (c) 2026 Paul Bakaus
* SPDX-License-Identifier: Apache-2.0
*
* GENERATED -- do not edit. Source: detect-antipatterns.mjs
* GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs
* Rebuild: node scripts/build-extension.js
*/
(function () {
@@ -53,22 +65,16 @@ console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length
// --- 2. Extract antipatterns.json ---
const rawSource = fs.readFileSync(SOURCE, 'utf-8');
const apMatch = rawSource.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (apMatch) {
// Convert JS object literals to JSON. Include description so the
// devtools panel can show the full rule explanation in tooltips —
// previously this dropped description and the panel had nothing to display.
const antipatterns = new Function(`return [${apMatch[1]}]`)();
const apJson = antipatterns.map(({ id, name, category, description }) => ({
id,
name,
category: category || 'quality',
description: description || '',
}));
fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${antipatterns.length} rules)`);
}
// Include description so the devtools panel can show the full rule explanation
// in tooltips.
const apJson = ANTIPATTERNS.map(({ id, name, category, description }) => ({
id,
name,
category: category || 'quality',
description: description || '',
}));
fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${ANTIPATTERNS.length} rules)`);
// --- 3. Zip packaging ---
+3 -8
View File
@@ -21,6 +21,7 @@ import { fileURLToPath } from 'url';
import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProjectArtifacts } from './lib/utils.js';
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
import { createAllZips } from './lib/zip.js';
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
// Sub-page generation is now handled by Astro content collections.
/**
@@ -48,14 +49,8 @@ function generateCounts(rootDir, skills, buildDir) {
commandCount = activeCommands.length;
}
// Count detection rules from impeccable package
const detectPkgPath = path.join(rootDir, 'cli/engine/detect-antipatterns.mjs');
const detectorSrc = fs.readFileSync(detectPkgPath, 'utf-8');
const ruleIds = new Set();
for (const match of detectorSrc.matchAll(/^\s+id: '([^']+)'/gm)) {
ruleIds.add(match[1]);
}
const detectionCount = ruleIds.size;
// Count detection rules from the detector registry.
const detectionCount = new Set(ANTIPATTERNS.map(rule => rule.id)).size;
// Write generated counts module
const genDir = path.join(rootDir, 'site/public/js/generated');
+5 -12
View File
@@ -5,7 +5,7 @@
* Single source of truth:
* - skill/SKILL.md → skill frontmatter + body
* - skill/reference/*.md → skill reference files
* - cli/engine/detect-antipatterns.mjs → ANTIPATTERNS array (parsed)
* - cli/engine/registry/antipatterns.mjs → ANTIPATTERNS registry
* - site/content/skills/{id}.md → optional editorial wrapper
* - site/content/tutorials/{slug}.md → full tutorial content
*/
@@ -14,6 +14,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { readSourceFiles, parseFrontmatter, replacePlaceholders } from './utils.js';
import { ANTIPATTERNS } from '../../cli/engine/registry/antipatterns.mjs';
import {
DETECTION_LAYERS,
VISUAL_EXAMPLES,
@@ -137,19 +138,11 @@ export const COMMAND_RELATIONSHIPS = {
};
/**
* Parse the ANTIPATTERNS array out of cli/engine/detect-antipatterns.mjs.
* Mirrors the trick in scripts/build.js validateAntipatternRules() so we
* don't have to run the browser-only module.
* Read the detector rule registry.
*/
export function readAntipatternRules(rootDir) {
const detectPath = path.join(rootDir, 'cli/engine/detect-antipatterns.mjs');
const src = fs.readFileSync(detectPath, 'utf-8');
const match = src.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (!match) {
throw new Error(`Could not extract ANTIPATTERNS from ${detectPath}`);
}
// eslint-disable-next-line no-new-func
return new Function(`return [${match[1]}]`)();
void rootDir;
return ANTIPATTERNS.slice();
}
/**