mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Compare commits
2
Commits
cli-v3.0.0
...
cli-v3.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
858b9bbea6 | ||
|
|
a9c15481a9 |
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .agents/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .agents/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .agents/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .cursor/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .cursor/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .cursor/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .gemini/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .gemini/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .gemini/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .github/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .github/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .github/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,14 @@
|
|||||||
"tests/detect-antipatterns.test.js",
|
"tests/detect-antipatterns.test.js",
|
||||||
"site/pages/slop/**"
|
"site/pages/slop/**"
|
||||||
],
|
],
|
||||||
"ignoreValues": [],
|
"ignoreValues": [
|
||||||
|
{
|
||||||
|
"rule": "bounce-easing",
|
||||||
|
"value": "bounce-ball",
|
||||||
|
"createdAt": "2026-06-15T04:15:03.164Z",
|
||||||
|
"reason": "User confirmed ball bounce animation is intentional"
|
||||||
|
}
|
||||||
|
],
|
||||||
"limits": {
|
"limits": {
|
||||||
"maxFindings": 5,
|
"maxFindings": 5,
|
||||||
"maxChars": 8000
|
"maxChars": 8000
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .kiro/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .kiro/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .kiro/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .opencode/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .opencode/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .opencode/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .pi/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .pi/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .pi/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .qoder/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .qoder/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .qoder/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .rovodev/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .rovodev/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .rovodev/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .trae-cn/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .trae-cn/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .trae-cn/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .trae/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .trae/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .trae/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "impeccable",
|
"name": "impeccable",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"author": "Paul Bakaus",
|
"author": "Paul Bakaus",
|
||||||
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
|
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|||||||
// ─── Update check ──────────────────────────────────────────────────────────
|
// ─── Update check ──────────────────────────────────────────────────────────
|
||||||
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
// Piggyback a lightweight skill-version check on the once-per-session boot.
|
||||||
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
|
||||||
// can offer `npx impeccable skills update`. Everything here is best-effort and
|
// can offer `npx impeccable update`. Everything here is best-effort and
|
||||||
// silent on failure: a network problem, sandbox, or missing cache must never
|
// silent on failure: a network problem, sandbox, or missing cache must never
|
||||||
// block context output or print an error.
|
// block context output or print an error.
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ function buildUpdateDirective(localVersion, latestVersion) {
|
|||||||
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
|
||||||
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
`(installed v${localVersion}, latest v${latestVersion}). ` +
|
||||||
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
|
||||||
`Update now? It runs \`npx impeccable skills update\`." ` +
|
`Update now? It runs \`npx impeccable update\`." ` +
|
||||||
`If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` +
|
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
|
||||||
`Either way, continue the current task without waiting, and do not raise this again.`
|
`Either way, continue the current task without waiting, and do not raise this again.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,9 +1084,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ const REGEX_MATCHERS = [
|
|||||||
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: () => 'animate-bounce (Tailwind)' },
|
fmt: () => 'animate-bounce (Tailwind)' },
|
||||||
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
|
{ id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
|
||||||
test: () => true,
|
test: () => true,
|
||||||
fmt: (m) => m[0] },
|
fmt: (m) => {
|
||||||
|
const token = m[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
return `animation: ${token || m[1].trim()}`;
|
||||||
|
} },
|
||||||
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
{ id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
|
||||||
test: (m) => {
|
test: (m) => {
|
||||||
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
|
||||||
|
|||||||
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
|
|||||||
// --- Motion ---
|
// --- Motion ---
|
||||||
|
|
||||||
// Bounce/elastic animation names
|
// Bounce/elastic animation names
|
||||||
const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
|
const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
|
||||||
if (bounceRe.test(html)) {
|
const bounceMatch = bounceRe.exec(html);
|
||||||
findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
|
if (bounceMatch) {
|
||||||
|
const animationToken = bounceMatch[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overshoot cubic-bezier
|
// Overshoot cubic-bezier
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,16 +453,22 @@ function isIgnoredFindingValue(finding, ignoreValues) {
|
|||||||
export function extractFindingIgnoreValue(finding) {
|
export function extractFindingIgnoreValue(finding) {
|
||||||
if (!finding || typeof finding !== 'object') return '';
|
if (!finding || typeof finding !== 'object') return '';
|
||||||
const rule = normalizeIgnoreRule(finding.antipattern);
|
const rule = normalizeIgnoreRule(finding.antipattern);
|
||||||
if (rule !== 'overused-font') return '';
|
if (rule !== 'overused-font' && rule !== 'bounce-easing') return '';
|
||||||
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding));
|
return normalizeIgnoreValue(extractFindingIgnoreValueRaw(finding, rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFindingIgnoreValueRaw(finding) {
|
function extractFindingIgnoreValueRaw(finding, rule = normalizeIgnoreRule(finding?.antipattern)) {
|
||||||
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
const direct = cleanIgnoreValueDisplay(finding.ignoreValue || finding.value || '');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
const candidates = [finding.detail, finding.snippet].filter((v) => typeof v === 'string' && v);
|
||||||
for (const text of candidates) {
|
for (const text of candidates) {
|
||||||
|
if (rule === 'bounce-easing') {
|
||||||
|
const motion = extractMotionIgnoreValue(text);
|
||||||
|
if (motion) return motion;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
const primary = text.match(/Primary font:\s*([^()\n;]+)/i);
|
||||||
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
if (primary) return cleanIgnoreValueDisplay(primary[1]);
|
||||||
|
|
||||||
@@ -482,6 +488,24 @@ function extractFindingIgnoreValueRaw(finding) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMotionIgnoreValue(text) {
|
||||||
|
const tailwind = text.match(/\banimate-bounce\b/i);
|
||||||
|
if (tailwind) return cleanIgnoreValueDisplay(tailwind[0]);
|
||||||
|
|
||||||
|
const bezier = text.match(/cubic-bezier\([^)]+\)/i);
|
||||||
|
if (bezier) return cleanIgnoreValueDisplay(bezier[0]);
|
||||||
|
|
||||||
|
const animation = text.match(/animation(?:-name)?\s*:\s*([^;\n]+)/i);
|
||||||
|
if (animation) {
|
||||||
|
const token = animation[1]
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
|
||||||
|
if (token) return cleanIgnoreValueDisplay(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function cleanIgnoreValueDisplay(value) {
|
function cleanIgnoreValueDisplay(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -524,7 +548,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|||||||
const shown = findings.slice(0, cap);
|
const shown = findings.slice(0, cap);
|
||||||
const remaining = total - shown.length;
|
const remaining = total - shown.length;
|
||||||
|
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections in ${display} (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
||||||
const lines = shown.map((f) => formatFindingLine(f));
|
const lines = shown.map((f) => formatFindingLine(f));
|
||||||
const more = remaining > 0
|
const more = remaining > 0
|
||||||
? `... and ${remaining} more (see /impeccable audit).`
|
? `... and ${remaining} more (see /impeccable audit).`
|
||||||
@@ -556,7 +580,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|||||||
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
const maxChars = Math.max(500, limits.maxChars || DEFAULT_CONFIG.limits.maxChars);
|
||||||
const cwd = opts.cwd || process.cwd();
|
const cwd = opts.cwd || process.cwd();
|
||||||
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
const total = realGroups.reduce((sum, group) => sum + group.findings.length, 0);
|
||||||
const header = `${ENVELOPE_PREFIX} Required design corrections across ${realGroups.length} files (${total} issue(s)):`;
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review across ${realGroups.length} files (${total} issue(s)):`;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let shownCount = 0;
|
let shownCount = 0;
|
||||||
|
|
||||||
@@ -968,7 +992,7 @@ export function renderPendingAck(filePath, knownFindings, opts = {}) {
|
|||||||
// `knownFindings` here are the cache strings like "side-tab:3".
|
// `knownFindings` here are the cache strings like "side-tab:3".
|
||||||
const sample = knownFindings.slice(0, 3).join(', ');
|
const sample = knownFindings.slice(0, 3).join(', ');
|
||||||
const more = count > 3 ? `, +${count - 3} more` : '';
|
const more = count > 3 ? `, +${count - 3} more` : '';
|
||||||
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} issue(s) flagged earlier this session (${sample}${more}). Address them before finalizing — the previous reminder still applies.`;
|
return `${ENVELOPE_PREFIX} Design hook scanned ${display}. Still has ${count} finding(s) flagged earlier this session (${sample}${more}). Handle them before finalizing — the previous reminder still applies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldEmitAckForFile(filePath) {
|
export function shouldEmitAckForFile(filePath) {
|
||||||
@@ -977,28 +1001,28 @@ export function shouldEmitAckForFile(filePath) {
|
|||||||
|
|
||||||
// The directive footer is the part of the hook output that steers model
|
// The directive footer is the part of the hook output that steers model
|
||||||
// behavior. Three intentional moves:
|
// behavior. Three intentional moves:
|
||||||
// 1. **Imperative, not advisory.** "Fix these..." beats "Consider
|
// 1. **Imperative, not advisory.** "Handle these..." beats "Consider
|
||||||
// revising..." which the model treats as a soft suggestion it can
|
// revising..." which the model treats as a soft suggestion it can
|
||||||
// override when the user asked for any kind of throwaway / demo UI.
|
// override when the user asked for any kind of throwaway / demo UI.
|
||||||
// 2. **Explicit exception clause.** Without it, the model will try to
|
// 2. **Explicit judgment clause.** Without it, the model will try to
|
||||||
// "fix" intentional bad fixtures, anti-pattern examples in docs, or
|
// "fix" intentional motion, bad fixtures, anti-pattern examples in
|
||||||
// test cases. Naming the exception inline beats hoping the model
|
// docs, or test cases. Naming the judgment inline beats hoping the
|
||||||
// infers it from context.
|
// model infers it from context.
|
||||||
// 3. **Acknowledgement instruction.** Hook output is injected as
|
// 3. **Acknowledgement instruction.** Hook output is injected as
|
||||||
// developer-role context, not a chat turn, so the user never sees the
|
// developer-role context, not a chat turn, so the user never sees the
|
||||||
// raw envelope. Asking the model to surface the fix in its reply is
|
// raw envelope. Asking the model to surface the resolution in its
|
||||||
// the cheapest way to make the feedback loop visible to the user.
|
// reply is the cheapest way to make the feedback loop visible.
|
||||||
function directiveFooter(display, opts = {}) {
|
function directiveFooter(display, opts = {}) {
|
||||||
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
|
||||||
const fileIgnoreGuidance = opts.grouped
|
const fileIgnoreGuidance = opts.grouped
|
||||||
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
? 'run `/impeccable hooks ignore-file <path>` for the specific file'
|
||||||
: `run \`${ignoreFileCommand}\``;
|
: `run \`${ignoreFileCommand}\``;
|
||||||
return [
|
return [
|
||||||
'Fix these in your next reply before finalizing. Acknowledge what you changed so the user sees the correction.',
|
'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
|
||||||
'',
|
'',
|
||||||
'Skip the fix only if the user explicitly asked for an intentionally bad UI, an anti-pattern example, a test fixture, or documentation of bad design. In that case, say so and continue.',
|
'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
|
||||||
'',
|
'',
|
||||||
`Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Do not add hook ignores unless the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
`Do not change intentional design just to satisfy the hook. Do not add source comments such as \`impeccable: ignore\`; those pollute the code and do not suppress hook findings. Persist hook ignores only after the user explicitly confirms the finding is intentional. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import '../styles/changelog-faq-kinpaku.css';
|
|||||||
<p class="cf-entry-lead">Project design hooks, deeper Live Mode support for Svelte and manual edits, and a broad detector accuracy pass across the skill, CLI, and extension.</p>
|
<p class="cf-entry-lead">Project design hooks, deeper Live Mode support for Svelte and manual edits, and a broad detector accuracy pass across the skill, CLI, and extension.</p>
|
||||||
<ul class="cf-items">
|
<ul class="cf-items">
|
||||||
<li><strong>Project design hooks.</strong> <code>/impeccable hooks</code> installs and repairs a project-local detector hook for Claude, Codex, and Cursor. Claude and Codex get post-edit reminders, Cursor can block proposed writes before they land, and <code>/impeccable hooks on</code> now handles manifest setup and consent instead of leaving users to wire files by hand.</li>
|
<li><strong>Project design hooks.</strong> <code>/impeccable hooks</code> installs and repairs a project-local detector hook for Claude, Codex, and Cursor. Claude and Codex get post-edit reminders, Cursor can block proposed writes before they land, and <code>/impeccable hooks on</code> now handles manifest setup and consent instead of leaving users to wire files by hand.</li>
|
||||||
<li><strong>Hook findings are actionable, not noisy.</strong> Hook runs track clean, pending, and fresh findings, cache duplicate reports, audit their own activity, and offer narrow ignore flows through <code>ignore-value</code>, <code>ignore-file</code>, and <code>ignore-rule</code>. Shared config lives in <code>.impeccable/config.json</code>, with local consent and overrides in <code>.impeccable/config.local.json</code>.</li>
|
<li><strong>Hook findings are actionable, not noisy.</strong> Hook runs track clean, pending, and fresh findings, cache duplicate reports, audit their own activity, and now frame detector output as findings requiring review: fix true issues, but use context judgment for intentional designs such as literal bounce motion. Narrow ignore flows through <code>ignore-value</code>, <code>ignore-file</code>, and <code>ignore-rule</code> keep confirmed exceptions out of source comments. Shared config lives in <code>.impeccable/config.json</code>, with local consent and overrides in <code>.impeccable/config.local.json</code>.</li>
|
||||||
<li><strong>Svelte-native Live Mode.</strong> Svelte and SvelteKit variants now preview as temporary framework components with params stored in <code>params.json</code>, then accept back into the selected source component. That keeps stateful pages closer to their real shape and avoids the HMR resets caused by string-injected previews.</li>
|
<li><strong>Svelte-native Live Mode.</strong> Svelte and SvelteKit variants now preview as temporary framework components with params stored in <code>params.json</code>, then accept back into the selected source component. That keeps stateful pages closer to their real shape and avoids the HMR resets caused by string-injected previews.</li>
|
||||||
<li><strong>Manual and browser Live Mode got sturdier.</strong> Manual text edits have dedicated evidence, apply, and discard routes; Live Mode preserves insertion anchors and mapped-list accept cleanup; and the browser payload is split into DOM helpers, UI primitives, vocabulary, and manual-apply modules instead of one giant script.</li>
|
<li><strong>Manual and browser Live Mode got sturdier.</strong> Manual text edits have dedicated evidence, apply, and discard routes; Live Mode preserves insertion anchors and mapped-list accept cleanup; and the browser payload is split into DOM helpers, UI primitives, vocabulary, and manual-apply modules instead of one giant script.</li>
|
||||||
<li><strong>Detector accuracy improved across the bundled skill.</strong> Hidden and unrendered elements are skipped in browser rules, sr-only and visually hidden text no longer trips <code>text-overflow</code>, repeated kicker false positives are reduced in card and list contexts, oversized H1 detection now requires viewport dominance, clipped overflow distinguishes decorative viewports from escaping content, OKLCH alpha parses correctly, Sass files count as CSS-like detector inputs, transparent borders or shadows no longer trigger the GPT thin-border rule, and page-level numbered-marker checks no longer treat JS, TS, JSX, TSX, or CSS implementation literals as visible page copy.</li>
|
<li><strong>Detector accuracy improved across the bundled skill.</strong> Hidden and unrendered elements are skipped in browser rules, sr-only and visually hidden text no longer trips <code>text-overflow</code>, repeated kicker false positives are reduced in card and list contexts, oversized H1 detection now requires viewport dominance, clipped overflow distinguishes decorative viewports from escaping content, OKLCH alpha parses correctly, Sass files count as CSS-like detector inputs, transparent borders or shadows no longer trigger the GPT thin-border rule, and page-level numbered-marker checks no longer treat JS, TS, JSX, TSX, or CSS implementation literals as visible page copy.</li>
|
||||||
@@ -83,6 +83,14 @@ import '../styles/changelog-faq-kinpaku.css';
|
|||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article id="cli-v3.0.1" class="cf-entry">
|
||||||
|
<header class="cf-entry-head"><span class="cf-version">CLI v3.0.1</span><span class="cf-date">June 15, 2026</span></header>
|
||||||
|
<ul class="cf-items">
|
||||||
|
<li><strong>Motion findings preserve the actual value.</strong> <code>bounce-easing</code> findings now report the concrete animation token, Tailwind class, or overshooting cubic-bezier value instead of collapsing animation names to a generic <code>bounce</code>. That gives hooks and audits enough information to suggest narrow <code>ignore-value</code> exceptions for intentional motion.</li>
|
||||||
|
<li><strong>Project hook guidance uses judgment first.</strong> Hook output now says findings require review, not automatic correction. Agents are told to fix real design problems, classify contextual false positives explicitly, and avoid changing intentional design just to satisfy the detector.</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
<article id="cli-v3.0.0" class="cf-entry">
|
<article id="cli-v3.0.0" class="cf-entry">
|
||||||
<header class="cf-entry-head"><span class="cf-version">CLI v3.0.0</span><span class="cf-date">June 14, 2026</span></header>
|
<header class="cf-entry-head"><span class="cf-version">CLI v3.0.0</span><span class="cf-date">June 14, 2026</span></header>
|
||||||
<ul class="cf-items">
|
<ul class="cf-items">
|
||||||
@@ -134,7 +142,7 @@ import '../styles/changelog-faq-kinpaku.css';
|
|||||||
<ul class="cf-items">
|
<ul class="cf-items">
|
||||||
<li><strong>Firefox build.</strong> The same detector, popup, DevTools panel, and per-rule toggles now ship as a Firefox add-on. <code>bun run build:extension</code> emits a Gecko-compatible package next to the Chrome one, with the background worker declared as an event page and a data-collection declaration that states what the extension already does: the scan runs in the page, and nothing leaves your machine.</li>
|
<li><strong>Firefox build.</strong> The same detector, popup, DevTools panel, and per-rule toggles now ship as a Firefox add-on. <code>bun run build:extension</code> emits a Gecko-compatible package next to the Chrome one, with the background worker declared as an event page and a data-collection declaration that states what the extension already does: the scan runs in the page, and nothing leaves your machine.</li>
|
||||||
<li><strong>Firefox DevTools paths are fixed.</strong> DevTools panel and sidebar URLs are root-relative in the Firefox manifest, so packaged builds can open their extension pages reliably.</li>
|
<li><strong>Firefox DevTools paths are fixed.</strong> DevTools panel and sidebar URLs are root-relative in the Firefox manifest, so packaged builds can open their extension pages reliably.</li>
|
||||||
<li><strong>Detector results match the latest engine.</strong> The overlay picks up the same false-positive fixes as the CLI: hidden-element skips, sr-only text-overflow handling, tighter repeated kicker, oversized H1, clipped-overflow, OKLCH alpha, Sass-adjacent CSS parsing, and transparent-border handling.</li>
|
<li><strong>Detector results match the latest engine.</strong> The overlay picks up the same false-positive fixes as the CLI: hidden-element skips, sr-only text-overflow handling, tighter repeated kicker, oversized H1, clipped-overflow, OKLCH alpha, Sass-adjacent CSS parsing, transparent-border handling, and exact animation values for bounce/easing findings.</li>
|
||||||
<li><strong>Scan responses are easier to correlate.</strong> Extension scan messages echo scan IDs back to the caller, and the store metadata and icon set were refreshed for the current 41-rule detector.</li>
|
<li><strong>Scan responses are easier to correlate.</strong> Extension scan messages echo scan IDs back to the caller, and the store metadata and icon set were refreshed for the current 41-rule detector.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
+12
-7
@@ -1040,15 +1040,20 @@ code {
|
|||||||
@keyframes toggle-drift { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(2px); } }
|
@keyframes toggle-drift { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(2px); } }
|
||||||
@keyframes toggle-snap { from { transform: translateX(0); fill: var(--color-mist); } to { transform: translateX(8px); fill: var(--color-accent); } }
|
@keyframes toggle-snap { from { transform: translateX(0); fill: var(--color-mist); } to { transform: translateX(8px); fill: var(--color-accent); } }
|
||||||
|
|
||||||
/* Motion (Gentle bob + smooth travel on hover) */
|
/* Motion (Gentle bob + full bounce on hover) */
|
||||||
.anim-squash-ball { transform-origin: 20px 20px; animation: ball-bob 2.5s ease-in-out infinite; }
|
.anim-squash-ball { transform-origin: 20px 20px; animation: ball-bob 2.5s ease-in-out infinite; }
|
||||||
.foundation-column:hover .anim-squash-ball { animation: travel-ball 1.5s var(--ease-out-quint) infinite; }
|
.foundation-column:hover .anim-squash-ball { animation: bounce-ball 1.5s linear infinite; }
|
||||||
@keyframes ball-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(3px); } }
|
@keyframes ball-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(3px); } }
|
||||||
@keyframes travel-ball {
|
@keyframes bounce-ball {
|
||||||
0% { transform: translateY(0); }
|
0% { transform: translateY(0); }
|
||||||
35% { transform: translateY(11px); }
|
6% { transform: translateY(0.5px); }
|
||||||
60% { transform: translateY(12px); }
|
18% { transform: translateY(4px); }
|
||||||
100% { transform: translateY(0); }
|
35% { transform: translateY(12px); }
|
||||||
|
42% { transform: translateY(12px) scaleX(1.3) scaleY(0.6); }
|
||||||
|
48% { transform: translateY(12px); }
|
||||||
|
65% { transform: translateY(4px); }
|
||||||
|
78% { transform: translateY(0.5px); }
|
||||||
|
88%, 100% { transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UX Writing (Cursor always blinks) */
|
/* UX Writing (Cursor always blinks) */
|
||||||
@@ -1081,7 +1086,7 @@ code {
|
|||||||
.foundation-card:hover .anim-res-line-1 { transform: translate(-7px, 4.75px) scaleX(0.65); }
|
.foundation-card:hover .anim-res-line-1 { transform: translate(-7px, 4.75px) scaleX(0.65); }
|
||||||
.foundation-card:hover .anim-res-line-2 { transform: translate(-6px, 4.25px) scaleX(0.6); }
|
.foundation-card:hover .anim-res-line-2 { transform: translate(-6px, 4.25px) scaleX(0.6); }
|
||||||
.foundation-card:hover .anim-toggle-move { animation: toggle-snap 0.35s var(--ease-in-out) forwards; }
|
.foundation-card:hover .anim-toggle-move { animation: toggle-snap 0.35s var(--ease-in-out) forwards; }
|
||||||
.foundation-card:hover .anim-squash-ball { animation: travel-ball 1.5s var(--ease-out-quint) infinite; }
|
.foundation-card:hover .anim-squash-ball { animation: bounce-ball 1.5s linear infinite; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Small tablet: 2-col grid */
|
/* Small tablet: 2-col grid */
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The hook itself never writes ignore config. Persist an exception only after the
|
|||||||
Prefer the narrowest exception:
|
Prefer the narrowest exception:
|
||||||
|
|
||||||
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
- If the finding line shows an exact `ignore-value` command, run that command. This writes shared `.impeccable/config.json` by default.
|
||||||
- For `overused-font`, use `ignore-value` when the user confirms a specific font. Do not use `ignore-rule overused-font` for a specific font.
|
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` when the user confirms the specific value. Do not use `ignore-rule overused-font` for a specific font.
|
||||||
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
- If the finding has no value-specific command, such as `side-tab`, prefer `ignore-file <path>` for the current file.
|
||||||
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
- Use `ignore-rule <id>` only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use `ignore-rule overused-font --all-values` only when the user asks to ignore overused fonts generally.
|
||||||
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
- Do not add source comments such as `impeccable: ignore`; inline comments pollute code and are not a supported suppression mechanism.
|
||||||
@@ -57,6 +57,12 @@ Example value-specific exception:
|
|||||||
node {{scripts_path}}/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
node {{scripts_path}}/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example intentional motion exception:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node {{scripts_path}}/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"
|
||||||
|
```
|
||||||
|
|
||||||
Example whole-rule font exception:
|
Example whole-rule font exception:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -333,8 +333,8 @@ function isInsideProject(filePath, cwd) {
|
|||||||
function cursorBlockMessage(findings, filePath, config, cwd) {
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
||||||
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
||||||
const blocked = rendered.replace(
|
const blocked = rendered.replace(
|
||||||
'[impeccable@1] Required design corrections',
|
'[impeccable@1] Design hook findings requiring review',
|
||||||
'[impeccable@1] Impeccable design hook blocked this write before it landed. Required design corrections',
|
'[impeccable@1] Impeccable design hook blocked this write before it landed. Design hook findings requiring review',
|
||||||
);
|
);
|
||||||
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
return blocked.length > 4000 ? `${blocked.slice(0, 3984)}\n...(truncated)` : blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user