Files
pbakaus_impeccable/scripts/build-extension.js
T
Paul BakausandClaude Opus 4.6 e1032b7285 Add icon-tile-stack rule and cross-validate engine against skill
A new icon-tile-stack detection (the canonical AI feature-card with a
small rounded-square icon container above a heading), backed by a
two-column TDD fixture, plus a single-source-of-truth design that ties
the engine to the impeccable skill so they can no longer drift silently.

Detection
- New icon-tile-stack rule (slop): heading's previousElementSibling is
  a 32–128px rounded-square element with a non-transparent background
  or border, contains an svg/icon-i child, and sits above (not next to)
  the heading. Excludes round avatars, wide thumbnails, side-by-side
  layouts, tiny icons, and hero images.
- Two-column fixture convention: a single icon-tile-stack.html with a
  flag column (4 cases) and pass column (6 cases), with snippet-text
  matching used by the fixture test.

Single source of truth
- Each ANTIPATTERNS entry can now declare skillSection + skillGuideline.
  18 of 25 rules carry these fields; the build's new
  validateAntipatternRules() in scripts/build.js fails if any declared
  skillGuideline isn't found verbatim in the right SKILL.md section.
- scripts/build-extension.js now includes the description field in
  extension/detector/antipatterns.json (it was previously dropped).
- The existing count validator was promoted from warn to error so
  command count drift fails the build the same way detection drift does.

Impeccable skill DON'Ts
- Added 4 new top-level DON'Ts that target real default AI behavior:
  single-font, flat-type-hierarchy, all-caps-body, line-length.
- Cut 7 new DON'Ts I had drafted (tight-leading, tiny-text, wide-tracking,
  justified-text, low-contrast, cramped-padding, skipped-heading) because
  they teach things every model already knows from CSS/a11y basics. The
  detector still catches all of them.

Stale count cleanup
- 22 commands → 21 across 17 references in HTML, README, NOTICE, AGENTS,
  plugin.json, marketplace.json (left over from the validate skill removal).
- Dropped the hand-coded "212 design guidelines" marketing copy on the
  homepage, which never mapped to any real count.

Sub-agent
- New private .claude/agents/anti-patterns.md captures the full TDD
  recipe, schema, plug-in points, jsdom constraints, and pre-commit
  checklist so future sessions can add rules end-to-end without
  re-investigating the wiring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:58:13 -07:00

90 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Builds the Chrome DevTools extension.
*
* 1. Generates the extension variant of the browser detector
* 2. Extracts antipatterns.json for the panel UI
* 3. Optionally packages as a .zip for Chrome Web Store
*
* Run: node scripts/build-extension.js
* node scripts/build-extension.js --zip
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
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, 'src/detect-antipatterns.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');
// --- 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
* Rebuild: node scripts/build-extension.js
*/
(function () {
if (typeof window === 'undefined') return;
${code}
})();
`;
fs.mkdirSync(path.dirname(DETECTOR_OUTPUT), { recursive: true });
fs.writeFileSync(DETECTOR_OUTPUT, output);
console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
// --- 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)`);
}
// --- 3. Zip packaging ---
if (process.argv.includes('--zip')) {
const archiver = (await import('archiver')).default;
const zipPath = path.join(ROOT, 'dist/impeccable-extension.zip');
fs.mkdirSync(path.dirname(zipPath), { recursive: true });
const zipStream = fs.createWriteStream(zipPath);
const archive = archiver('zip', { zlib: { level: 9 } });
archive.pipe(zipStream);
archive.directory(EXT_DIR, false);
await archive.finalize();
const size = fs.statSync(zipPath).size;
console.log(`Packaged ${path.relative(ROOT, zipPath)} (${(size / 1024).toFixed(1)} KB)`);
}