mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7b9deab21 | ||
|
|
cb670ceb23 | ||
|
|
1828415f84 |
@@ -4,7 +4,7 @@ description: Use when the user wants to design, redesign, shape, critique, audit
|
||||
version: 4.1.2
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .agents/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .agents/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -10,7 +10,7 @@ allowed-tools:
|
||||
- Bash(node .claude/skills/impeccable/scripts/*)
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -70,20 +70,16 @@ Your measured rendition prior: warm, bookish, family, and child-facing subjects
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .claude/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .claude/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -5,7 +5,7 @@ version: 4.1.2
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .cursor/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .cursor/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Use when the user wants to design, redesign, shape, critique, audit
|
||||
version: 4.1.2
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .gemini/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .gemini/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -7,7 +7,7 @@ argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layo
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .github/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .github/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: PR Sheriff
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
schedule:
|
||||
# Daily UTC afternoon pass. The script uses an aggressive 7/14 day window:
|
||||
# warn contributor-blocked PRs after 7 days open, close after 14.
|
||||
@@ -26,25 +24,7 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
issue-inbox:
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mark issue for triage
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
gh label create "needs triage" \
|
||||
--repo "$GH_REPO" \
|
||||
--color "d4c5f9" \
|
||||
--description "New or reopened issue awaiting maintainer triage" \
|
||||
--force
|
||||
gh issue edit "$ISSUE_NUMBER" --repo "$GH_REPO" --add-label "needs triage"
|
||||
|
||||
sheriff:
|
||||
if: github.event_name != 'issues'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -69,5 +49,4 @@ jobs:
|
||||
--warning-days 7 \
|
||||
--close-days 14 \
|
||||
--maintainers "pbakaus" \
|
||||
--regular-contributors "pbakaus,abdulwahabone" \
|
||||
--auto-close-regulars
|
||||
--regular-contributors "pbakaus,abdulwahabone"
|
||||
|
||||
@@ -10,7 +10,7 @@ allowed-tools:
|
||||
- Bash(node .grok/skills/impeccable/scripts/*)
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .grok/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .grok/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -5,7 +5,7 @@ version: 4.1.2
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .hermes/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .hermes/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -5,7 +5,7 @@ version: 4.1.2
|
||||
license: Apache 2.0
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .kiro/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .kiro/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -10,7 +10,7 @@ allowed-tools:
|
||||
- Bash(node .opencode/skills/impeccable/scripts/*)
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .opencode/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .opencode/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
@@ -329,37 +329,47 @@ function stripBold(s) {
|
||||
function extractNamedRules(lines) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
const addRule = (name, body, { allowDuplicate = false } = {}) => {
|
||||
const key = name.toLowerCase();
|
||||
if (!allowDuplicate && seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rules.push({ name, body });
|
||||
};
|
||||
|
||||
// Style A (Impeccable): "**The X Rule.** body body body" — can span lines.
|
||||
const joined = lines.join('\n');
|
||||
const inlineMatches = [...joined.matchAll(/\*\*(The [^*]+?Rule)\.\*\*/g)];
|
||||
const inlineStart = /\*\*(The [^*]+?Rule)\.\*\*/g;
|
||||
const inlineMatches = [];
|
||||
let m;
|
||||
while ((m = inlineStart.exec(joined)) !== null) {
|
||||
inlineMatches.push({ name: m[1], start: m.index, end: inlineStart.lastIndex });
|
||||
}
|
||||
for (let i = 0; i < inlineMatches.length; i++) {
|
||||
const match = inlineMatches[i];
|
||||
const bodyEnd = inlineMatches[i + 1]?.index ?? joined.length;
|
||||
const mm = inlineMatches[i];
|
||||
const bodyEnd = i + 1 < inlineMatches.length ? inlineMatches[i + 1].start : joined.length;
|
||||
const body = joined
|
||||
.slice(match.index + match[0].length, bodyEnd)
|
||||
.slice(mm.end, bodyEnd)
|
||||
.replace(/\n##[^\n]*$/s, '')
|
||||
.replace(/\n###[^\n]*$/s, '')
|
||||
.trim();
|
||||
// Preserve the inline format's historical behavior: repeated inline rules
|
||||
// remain visible, while the later heading and bullet formats dedupe.
|
||||
addRule(stripBold(match[1]).trim(), stripBold(body), { allowDuplicate: true });
|
||||
const name = stripBold(mm.name).trim();
|
||||
seen.add(name.toLowerCase());
|
||||
rules.push({ name, body: stripBold(body) });
|
||||
}
|
||||
|
||||
// Style B (Stitch): `### The "X" Rule` or `### The X Fallback`, body is the
|
||||
// bullets/paragraphs until the next heading. Accept Rule / Fallback / Principle.
|
||||
for (const subsection of splitSubsections(lines).slice(1)) {
|
||||
const headerName = stripBold(subsection.name).replace(/["“”]/g, '').trim();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const h3 = lines[i].match(/^###\s+(.+?)\s*$/);
|
||||
if (!h3) continue;
|
||||
const headerName = stripBold(h3[1]).replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.*\b(Rule|Fallback|Principle)\b/i.test(headerName)) continue;
|
||||
if (seen.has(headerName.toLowerCase())) continue;
|
||||
|
||||
const body = stripBold(subsection.lines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) addRule(headerName, body);
|
||||
const bodyLines = [];
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (/^##\s|^###\s/.test(lines[j])) break;
|
||||
bodyLines.push(lines[j]);
|
||||
}
|
||||
const body = stripBold(bodyLines.join('\n').replace(/\n+/g, ' ')).trim();
|
||||
if (body) {
|
||||
seen.add(headerName.toLowerCase());
|
||||
rules.push({ name: headerName, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Style C (Stitch bullet form): "* **The Layering Principle:** body"
|
||||
@@ -369,7 +379,9 @@ function extractNamedRules(lines) {
|
||||
if (!mm) continue;
|
||||
const nameRaw = mm[1].replace(/[.:]\s*$/, '').replace(/["“”]/g, '').trim();
|
||||
if (!/^The\b.+\b(Rule|Fallback|Principle)$/i.test(nameRaw)) continue;
|
||||
addRule(nameRaw, stripBold(mm[2]).trim());
|
||||
if (seen.has(nameRaw.toLowerCase())) continue;
|
||||
seen.add(nameRaw.toLowerCase());
|
||||
rules.push({ name: nameRaw, body: stripBold(mm[2]).trim() });
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
@@ -8,7 +8,7 @@ allowed-tools:
|
||||
- Bash(node .pi/skills/impeccable/scripts/*)
|
||||
---
|
||||
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
|
||||
|
||||
Core principles:
|
||||
- Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide).
|
||||
|
||||
@@ -32,7 +32,7 @@ The first argument is the action. Defaults to `status`.
|
||||
| `ignore-value <id> <value> [--shared] [--reason "..."]` | Append a rule/value suppression to shared `.impeccable/config.json`. |
|
||||
| `ignore-value <id> <value> --local [--reason "..."]` | Append a private rule/value suppression to `.impeccable/config.local.json`. |
|
||||
| `ignore-value <id> "*" --file <glob> [--file <glob>...]` | Turn one rule off in matching files only, leaving it active everywhere else. Repeat `--file`, or use `--file=<glob>` / `--files=<glob>`. A bare `"*"` with no `--file` is refused: use `ignore-rule <id>` if you really mean project-wide. |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue, and remove the hook's entries from every provider manifest `on` installs, the committed Copilot file included (a team-shared `settings.json` that `on` never writes is never touched). |
|
||||
| `reset` | Delete the project config, dedup cache, and Cursor pending queue. |
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
@@ -68,20 +68,16 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su
|
||||
|
||||
## 5. Record the decision
|
||||
|
||||
Before code, record the chosen direction as a development-only contract under `## Direction contract` in the relevant surface brief. A direction contract is durable route or artifact strategy, so create or update the brief even when no other surface strategy needs persistence. Keep the contract to six short blocks and 150 words at most. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The surface brief is the reminder later agents reload across edits and sessions: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
Never copy the direction contract into implementation source or any browser-delivered artifact. This includes HTML or framework comments, hidden DOM, `<template>` elements, `data-*` attributes, rendered JSX or TSX output, serialized props or state, React Server Component payloads, client bundles, metadata or JSON-LD, accessibility-only text, and files served beside the artifact. A compiler or optimizer removing development metadata is not a safety boundary. Reviewers and documenters receive the contract from the surface brief.
|
||||
Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract.
|
||||
|
||||
On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md.
|
||||
|
||||
Read the existing surface brief before updating it:
|
||||
If the work establishes durable strategy for a route or artifact, read its existing surface brief, then update it:
|
||||
|
||||
`node .pi/skills/impeccable/scripts/surface-brief.mjs read <primary-target>`
|
||||
|
||||
`node .pi/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
|
||||
|
||||
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
|
||||
|
||||
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
|
||||
|
||||
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior.
|
||||
|
||||
@@ -70,27 +70,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -1605,7 +1591,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -5204,7 +5190,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -5449,7 +5436,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -5468,10 +5456,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
|
||||
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import {
|
||||
checkSourceDesignSystem,
|
||||
collectStaticDesignSystemFindings,
|
||||
@@ -51,7 +51,9 @@ function checkStaticPageTypography(document, window) {
|
||||
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
|
||||
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
||||
if (!hasText) continue;
|
||||
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
|
||||
const ff = window.getComputedStyle(el).fontFamily || '';
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
isBrandFontOnOwnDomain,
|
||||
primaryFontFace,
|
||||
} from '../shared/constants.mjs';
|
||||
import {
|
||||
CSS_NAMED_COLORS,
|
||||
@@ -332,7 +331,7 @@ function checkIconTile(opts) {
|
||||
function resolveSerif(fontFamily) {
|
||||
if (!fontFamily) return { primary: null, isSerif: false };
|
||||
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
|
||||
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
|
||||
if (!primary) return { primary: null, isSerif: false };
|
||||
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
|
||||
if (tokens.includes('serif')) return { primary, isSerif: true };
|
||||
@@ -3931,7 +3930,8 @@ function checkTypography() {
|
||||
const style = getComputedStyle(el);
|
||||
const ff = style.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (!primary) continue;
|
||||
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
|
||||
totalTextElements++;
|
||||
@@ -4176,7 +4176,8 @@ function checkPageTypography(doc, win) {
|
||||
if (rule.type !== 1) continue;
|
||||
const ff = rule.style?.fontFamily;
|
||||
if (!ff) continue;
|
||||
const primary = primaryFontFace(ff);
|
||||
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
|
||||
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
@@ -4195,10 +4196,11 @@ function checkPageTypography(doc, win) {
|
||||
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
|
||||
let fm;
|
||||
while ((fm = ffRe.exec(html)) !== null) {
|
||||
const primary = primaryFontFace(fm[1]);
|
||||
if (primary) {
|
||||
fonts.add(primary);
|
||||
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
|
||||
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
||||
if (f && !GENERIC_FONTS.has(f)) {
|
||||
fonts.add(f);
|
||||
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +56,13 @@ function isBrandFontOnOwnDomain(font) {
|
||||
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
||||
}
|
||||
|
||||
// Overused-font primary selection skips only CSS generics so a system stack
|
||||
// keeps the system face as primary; GENERIC_FONTS still includes platform
|
||||
// faces for design-system/serif resolution.
|
||||
const CSS_GENERIC_FONTS = new Set([
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
const GENERIC_FONTS = new Set([
|
||||
...CSS_GENERIC_FONTS,
|
||||
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
||||
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
||||
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
||||
'inherit', 'initial', 'unset', 'revert',
|
||||
]);
|
||||
|
||||
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
|
||||
return String(fontFamily || '')
|
||||
.split(',')
|
||||
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
|
||||
.find(f => f && !skip.has(f)) || null;
|
||||
}
|
||||
|
||||
// WCAG large text thresholds are defined in points: 18pt normal text and
|
||||
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
||||
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
||||
@@ -118,7 +104,6 @@ export {
|
||||
BRAND_FONT_DOMAINS,
|
||||
isBrandFontOnOwnDomain,
|
||||
GENERIC_FONTS,
|
||||
primaryFontFace,
|
||||
WCAG_LARGE_TEXT_PX,
|
||||
WCAG_LARGE_BOLD_TEXT_PX,
|
||||
EM_DASH_FLOOR,
|
||||
|
||||
@@ -21,24 +21,22 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (args.includes('--scan')) {
|
||||
if (scanMode) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -61,7 +59,7 @@ if (args.includes('--scan')) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -70,7 +68,8 @@ if (args.includes('--scan')) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const type = imageType(buf);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -88,26 +87,22 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return { chunks, prompt };
|
||||
off += 12 + len;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -126,34 +121,48 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (type === 'png') {
|
||||
if (isPng) {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (type === 'jpeg') {
|
||||
} else if (isJpeg) {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -770,22 +770,9 @@ function reset(cwd) {
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// `on` writes three things: config, consent, and hook entries in the
|
||||
// provider manifests. Reset must undo all three (issue #512): a leftover
|
||||
// manifest entry kept invoking the hook after the config that said "off"
|
||||
// was deleted. Local destRel only, since `on` never writes the team-shared
|
||||
// sharedDestRel. No skill-folder gate: a reset mid-uninstall (skill files
|
||||
// gone, manifest still wired) is the case that most needs the prune.
|
||||
const pruned = [];
|
||||
for (const target of HOOK_MANIFEST_TARGETS) {
|
||||
try {
|
||||
if (pruneImpeccableHookFromManifest(path.join(cwd, target.destRel))) pruned.push(target.provider);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const parts = [];
|
||||
if (removed.length) parts.push(`Reset design hook config and cache (removed: ${removed.join(', ')}).`);
|
||||
if (pruned.length) parts.push(`Removed hook entries from: ${pruned.join(', ')}.`);
|
||||
return parts.length ? parts.join(' ') : 'No hook config or cache to remove. Already at defaults.';
|
||||
return removed.length
|
||||
? `Reset design hook config and cache (removed: ${removed.join(', ')}).`
|
||||
: 'No hook config or cache to remove. Already at defaults.';
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user