@bastani/atomic 0.9.7 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/builtin/cursor/CHANGELOG.md +12 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +12 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +14 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/{effective-liteparse → liteparse}/SKILL.md +4 -4
- package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +21 -5
- package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +311 -12
- package/dist/builtin/web-access/CHANGELOG.md +12 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +24 -0
- package/dist/builtin/workflows/README.md +2 -2
- package/dist/builtin/workflows/builtin/goal-prompts.ts +9 -7
- package/dist/builtin/workflows/builtin/goal-reducer.ts +9 -24
- package/dist/builtin/workflows/builtin/goal-review.ts +17 -36
- package/dist/builtin/workflows/builtin/ralph-core.ts +1 -3
- package/dist/builtin/workflows/builtin/ralph-review-gate.ts +31 -53
- package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +10 -4
- package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -4
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +37 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +2 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +20 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +3 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +31 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +7 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +11 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +4 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +2 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +38 -16
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +33 -19
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +9 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +2 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +16 -9
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +59 -14
- package/docs/quickstart.md +1 -1
- package/docs/workflows.md +4 -4
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
- /package/dist/builtin/subagents/skills/{effective-liteparse → liteparse}/scripts/search.py +0 -0
|
@@ -23,6 +23,7 @@ import os from 'node:os';
|
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
import { parseTargetOptions } from './lib/target-args.mjs';
|
|
26
|
+
import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
|
|
26
27
|
|
|
27
28
|
const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
|
|
28
29
|
const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
|
|
@@ -902,7 +903,7 @@ async function cli() {
|
|
|
902
903
|
'or wording that clearly maps to a from-scratch build/shape flow, load ' +
|
|
903
904
|
'reference/init.md and write PRODUCT.md first; for any other (scoped) ' +
|
|
904
905
|
'command against existing code, proceed using the code as context and ' +
|
|
905
|
-
|
|
906
|
+
`offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`,
|
|
906
907
|
];
|
|
907
908
|
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
|
|
908
909
|
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Critique persistence helper.
|
|
4
4
|
*
|
|
5
|
-
* Each run
|
|
5
|
+
* Each critique run writes a per-target snapshot to
|
|
6
6
|
* .impeccable/critique/<timestamp>__<slug>.md
|
|
7
7
|
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* The polish workflow reads the latest matching snapshot at start as its
|
|
10
10
|
* fix backlog. No other skill auto-reads critique output.
|
|
11
11
|
*
|
|
12
12
|
* The slug is derived mechanically from the *resolved* primary artifact
|
package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js
CHANGED
|
@@ -1284,14 +1284,30 @@ function checkHtmlPatterns(html) {
|
|
|
1284
1284
|
}
|
|
1285
1285
|
}
|
|
1286
1286
|
|
|
1287
|
+
function stripHtmlBlocks(content, tagNames) {
|
|
1288
|
+
let out = String(content);
|
|
1289
|
+
for (const tagName of tagNames) {
|
|
1290
|
+
const opener = '<' + tagName;
|
|
1291
|
+
const closer = '</' + tagName;
|
|
1292
|
+
while (true) {
|
|
1293
|
+
const lower = out.toLowerCase();
|
|
1294
|
+
const start = lower.indexOf(opener);
|
|
1295
|
+
if (start === -1) break;
|
|
1296
|
+
const openEnd = lower.indexOf('>', start + opener.length);
|
|
1297
|
+
const closeStart = openEnd === -1 ? -1 : lower.indexOf(closer, openEnd + 1);
|
|
1298
|
+
const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + closer.length);
|
|
1299
|
+
if (openEnd === -1 || closeStart === -1 || closeEnd === -1) break;
|
|
1300
|
+
out = out.slice(0, start) + ' ' + out.slice(closeEnd + 1);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1287
1306
|
// --- Provider tells (gated): "X theater" framing copy (GPT) ---
|
|
1288
1307
|
// Lives here (regex-on-HTML) rather than in the text-content analyzers so it
|
|
1289
1308
|
// runs in the bundled browser path too, not just the CLI/static path.
|
|
1290
1309
|
{
|
|
1291
|
-
const bodyText = html
|
|
1292
|
-
.replace(/<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/gi, ' ')
|
|
1293
|
-
.replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/gi, ' ')
|
|
1294
|
-
.replace(/<[^>]+>/g, ' ');
|
|
1310
|
+
const bodyText = stripHtmlBlocks(html, ['script', 'style']).replace(/<[^>]+>/g, ' ');
|
|
1295
1311
|
const tm = /\b(\w+)\s+theater\b/i.exec(bodyText);
|
|
1296
1312
|
if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` });
|
|
1297
1313
|
}
|
package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
|
|
2
2
|
import { isNeutralColor } from '../../shared/color.mjs';
|
|
3
3
|
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
|
4
4
|
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
|
5
|
-
import { isFullPage } from '../../shared/page.mjs';
|
|
5
|
+
import { isFullPage, stripHtmlBlocks } from '../../shared/page.mjs';
|
|
6
6
|
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
|
7
7
|
import { finding } from '../../findings.mjs';
|
|
8
8
|
import { filterByProviders } from '../../registry/antipatterns.mjs';
|
|
@@ -19,10 +19,7 @@ const isSafeElement = (line) => /<(?:blockquote|nav[\s>]|pre[\s>]|code[\s>]|a\s|
|
|
|
19
19
|
/** Strip HTML to plain text — drops script/style/comments/tags so
|
|
20
20
|
* content-text analyzers don't false-positive on code or CSS. */
|
|
21
21
|
function stripHtmlToText(html) {
|
|
22
|
-
return html
|
|
23
|
-
.replace(/<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/gi, ' ')
|
|
24
|
-
.replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/gi, ' ')
|
|
25
|
-
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
22
|
+
return stripHtmlBlocks(html, ['script', 'style'])
|
|
26
23
|
.replace(/<[^>]+>/g, ' ')
|
|
27
24
|
.replace(/\s+/g, ' ');
|
|
28
25
|
}
|
|
@@ -352,7 +349,7 @@ function extractStyleBlocks(content, ext) {
|
|
|
352
349
|
ext = ext.toLowerCase();
|
|
353
350
|
if (ext !== '.vue' && ext !== '.svelte') return [];
|
|
354
351
|
const blocks = [];
|
|
355
|
-
const re = /<style[^>]*>([\s\S]*?)<\/style
|
|
352
|
+
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
356
353
|
let m;
|
|
357
354
|
while ((m = re.exec(content)) !== null) {
|
|
358
355
|
const before = content.substring(0, m.index);
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
relativeLuminance,
|
|
20
20
|
} from '../shared/color.mjs';
|
|
21
21
|
import { extractGoogleFontFamilies } from '../shared/fonts.mjs';
|
|
22
|
+
import { stripHtmlBlocks } from '../shared/page.mjs';
|
|
22
23
|
|
|
23
24
|
const DETECTOR_IS_BROWSER = typeof window !== 'undefined';
|
|
24
25
|
|
|
@@ -614,10 +615,7 @@ function checkHtmlPatterns(html) {
|
|
|
614
615
|
// Lives here (regex-on-HTML) rather than in the text-content analyzers so it
|
|
615
616
|
// runs in the bundled browser path too, not just the CLI/static path.
|
|
616
617
|
{
|
|
617
|
-
const bodyText = html
|
|
618
|
-
.replace(/<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/gi, ' ')
|
|
619
|
-
.replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/gi, ' ')
|
|
620
|
-
.replace(/<[^>]+>/g, ' ');
|
|
618
|
+
const bodyText = stripHtmlBlocks(html, ['script', 'style']).replace(/<[^>]+>/g, ' ');
|
|
621
619
|
const tm = /\b(\w+)\s+theater\b/i.exec(bodyText);
|
|
622
620
|
if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` });
|
|
623
621
|
}
|
|
@@ -1,12 +1,35 @@
|
|
|
1
|
+
/** Remove complete script/style elements and comments without regex-based HTML sanitization. */
|
|
2
|
+
function stripHtmlBlocks(content, tagNames = []) {
|
|
3
|
+
let out = String(content);
|
|
4
|
+
for (const tagName of tagNames) {
|
|
5
|
+
const opener = `<${tagName}`;
|
|
6
|
+
const closer = `</${tagName}`;
|
|
7
|
+
while (true) {
|
|
8
|
+
const lower = out.toLowerCase();
|
|
9
|
+
const start = lower.indexOf(opener);
|
|
10
|
+
if (start === -1) break;
|
|
11
|
+
const openEnd = lower.indexOf('>', start + opener.length);
|
|
12
|
+
const closeStart = openEnd === -1 ? -1 : lower.indexOf(closer, openEnd + 1);
|
|
13
|
+
const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + closer.length);
|
|
14
|
+
if (openEnd === -1 || closeStart === -1 || closeEnd === -1) break;
|
|
15
|
+
out = out.slice(0, start) + ' ' + out.slice(closeEnd + 1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
while (true) {
|
|
19
|
+
const start = out.indexOf('<!--');
|
|
20
|
+
if (start === -1) break;
|
|
21
|
+
const normalEnd = out.indexOf('-->', start + 4);
|
|
22
|
+
const bangEnd = out.indexOf('--!>', start + 4);
|
|
23
|
+
const end = normalEnd === -1 ? bangEnd : bangEnd === -1 ? normalEnd : Math.min(normalEnd, bangEnd);
|
|
24
|
+
if (end === -1) break;
|
|
25
|
+
out = out.slice(0, start) + out.slice(end + (end === bangEnd ? 4 : 3));
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
1
30
|
/** Check if content looks like a full page (not a component/partial) */
|
|
2
31
|
function isFullPage(content) {
|
|
3
|
-
|
|
4
|
-
let previous;
|
|
5
|
-
do {
|
|
6
|
-
previous = stripped;
|
|
7
|
-
stripped = stripped.replace(/<!--[\s\S]*?-->/g, '');
|
|
8
|
-
} while (stripped !== previous);
|
|
9
|
-
return /<!doctype\s|<html[\s>]|<head[\s>]/i.test(stripped);
|
|
32
|
+
return /<!doctype\s|<html[\s>]|<head[\s>]/i.test(stripHtmlBlocks(content));
|
|
10
33
|
}
|
|
11
34
|
|
|
12
|
-
export { isFullPage };
|
|
35
|
+
export { isFullPage, stripHtmlBlocks };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The Impeccable hooks command manages the design hook runtime
|
|
4
4
|
* via the `hook` key and shared detector ignores via the `detector` key in
|
|
5
5
|
* .impeccable/config.json / .impeccable/config.local.json.
|
|
6
6
|
*
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
|
+
import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
|
|
24
25
|
|
|
25
26
|
import {
|
|
26
27
|
getConfigPath,
|
|
@@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) {
|
|
|
184
185
|
const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};
|
|
185
186
|
const existingHook = stripDetectorKeys(hookSection(existing));
|
|
186
187
|
// Merge over the existing hook object so fields the merge helpers don't manage
|
|
187
|
-
// (consent, quiet, auditLog) survive
|
|
188
|
+
// (consent, quiet, auditLog) survive an Impeccable hooks edit.
|
|
188
189
|
const next = { ...existing, hook: { ...existingHook, ...hookConfig } };
|
|
189
190
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
190
191
|
fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');
|
|
@@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) {
|
|
|
513
514
|
function addIgnoreRule(cwd, args) {
|
|
514
515
|
const parsed = parseIgnoreRuleArgs(args);
|
|
515
516
|
const rule = parsed.rule;
|
|
516
|
-
if (!rule) throw new Error(
|
|
517
|
+
if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`);
|
|
517
518
|
if (rule === 'overused-font' && !parsed.allValues) {
|
|
518
|
-
throw new Error(
|
|
519
|
+
throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font <font> for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`);
|
|
519
520
|
}
|
|
520
521
|
const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
|
|
521
522
|
if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
|
|
@@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) {
|
|
|
524
525
|
}
|
|
525
526
|
|
|
526
527
|
function addIgnoreFile(cwd, glob) {
|
|
527
|
-
if (!glob) throw new Error(
|
|
528
|
+
if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`);
|
|
528
529
|
const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
|
|
529
530
|
if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
|
|
530
531
|
writeDetectorConfig(cwd, config);
|
|
@@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) {
|
|
|
569
570
|
function addIgnoreValue(cwd, args) {
|
|
570
571
|
const parsed = parseIgnoreValueArgs(args);
|
|
571
572
|
if (!parsed.rule || !parsed.value) {
|
|
572
|
-
throw new Error(
|
|
573
|
+
throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`);
|
|
573
574
|
}
|
|
574
575
|
|
|
575
576
|
if (parsed.shared && parsed.local) {
|
|
@@ -41,6 +41,7 @@ import os from 'node:os';
|
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
43
43
|
import { extractPlatform, loadContext } from './context.mjs';
|
|
44
|
+
import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
|
|
44
45
|
|
|
45
46
|
const __filename = fileURLToPath(import.meta.url);
|
|
46
47
|
const __dirname = path.dirname(__filename);
|
|
@@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) {
|
|
|
661
662
|
}
|
|
662
663
|
|
|
663
664
|
export function suppressionNotice(filePath) {
|
|
664
|
-
return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run
|
|
665
|
+
return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`;
|
|
665
666
|
}
|
|
666
667
|
|
|
667
668
|
// Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation.
|
|
@@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
|
|
|
877
878
|
const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
|
|
878
879
|
const lines = shown.map((f) => formatFindingLine(f));
|
|
879
880
|
const more = remaining > 0
|
|
880
|
-
? `... and ${remaining} more (see
|
|
881
|
+
? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).`
|
|
881
882
|
: null;
|
|
882
883
|
const footer = directiveFooter(display);
|
|
883
884
|
|
|
@@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
|
|
|
921
922
|
shownCount += shown.length;
|
|
922
923
|
const hidden = group.findings.length - shown.length;
|
|
923
924
|
if (hidden > 0) {
|
|
924
|
-
lines.push(`- ... ${hidden} more in ${display} (see
|
|
925
|
+
lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`);
|
|
925
926
|
}
|
|
926
927
|
}
|
|
927
928
|
|
|
@@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) {
|
|
|
937
938
|
const assemble = (linesArr, omitted) => [
|
|
938
939
|
header,
|
|
939
940
|
...linesArr,
|
|
940
|
-
...(omitted ? [
|
|
941
|
+
...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []),
|
|
941
942
|
'',
|
|
942
943
|
footer,
|
|
943
944
|
].join('\n');
|
|
@@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) {
|
|
|
970
971
|
let assembled = assemble(working, moreText);
|
|
971
972
|
while (assembled.length > maxChars && working.length > 1) {
|
|
972
973
|
working.pop();
|
|
973
|
-
moreText =
|
|
974
|
+
moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`;
|
|
974
975
|
assembled = assemble(working, moreText);
|
|
975
976
|
}
|
|
976
977
|
if (assembled.length > maxChars) {
|
|
@@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) {
|
|
|
1002
1003
|
const value = extractFindingIgnoreValueRaw(finding);
|
|
1003
1004
|
const valueArg = quoteCommandArg(value);
|
|
1004
1005
|
const reason = quoteCommandArg(`User confirmed ${value} is intentional`);
|
|
1005
|
-
return
|
|
1006
|
+
return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`;
|
|
1006
1007
|
}
|
|
1007
1008
|
|
|
1008
1009
|
function quoteCommandArg(value) {
|
|
@@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) {
|
|
|
1447
1448
|
|
|
1448
1449
|
export function appendDesignSystemNote(text, scanOptions) {
|
|
1449
1450
|
if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text;
|
|
1450
|
-
return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run
|
|
1451
|
+
return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`;
|
|
1451
1452
|
}
|
|
1452
1453
|
|
|
1453
1454
|
// The directive footer is the part of the hook output that steers model
|
|
@@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) {
|
|
|
1464
1465
|
// raw envelope. Asking the model to surface the resolution in its
|
|
1465
1466
|
// reply is the cheapest way to make the feedback loop visible.
|
|
1466
1467
|
function directiveFooter(display, opts = {}) {
|
|
1467
|
-
const ignoreFileCommand =
|
|
1468
|
+
const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`;
|
|
1468
1469
|
const fileIgnoreGuidance = opts.grouped
|
|
1469
|
-
?
|
|
1470
|
+
? `run \`${IMPECCABLE_COMMAND} hooks ignore-file <path>\` for the specific file`
|
|
1470
1471
|
: `run \`${ignoreFileCommand}\``;
|
|
1471
1472
|
return [
|
|
1472
1473
|
'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.',
|
|
1473
1474
|
'',
|
|
1474
1475
|
'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.',
|
|
1475
1476
|
'',
|
|
1476
|
-
`Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable <rule>\` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact
|
|
1477
|
+
`Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable <rule>\` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} 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_COMMAND} 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_COMMAND} hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`,
|
|
1477
1478
|
].join('\n');
|
|
1478
1479
|
}
|
|
1479
1480
|
|
|
@@ -46,7 +46,7 @@ function isGitIgnored(absPath, cwd) {
|
|
|
46
46
|
stdio: 'ignore',
|
|
47
47
|
});
|
|
48
48
|
return true; // exit 0 = ignored
|
|
49
|
-
} catch {
|
|
49
|
+
} catch (err) {
|
|
50
50
|
// Exit code 1 = not ignored. Exit code 128 = not a git repo or other error.
|
|
51
51
|
// In both cases, treat as "not known to be ignored."
|
|
52
52
|
return false;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Source scripts default to slash commands. The provider build replaces only
|
|
2
|
+
// this exact declaration, avoiding heuristic rewrites across executable code.
|
|
3
|
+
export const IMPECCABLE_COMMAND_PREFIX = "$";
|
|
4
|
+
export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`;
|
|
@@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re
|
|
|
32
32
|
}));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) {
|
|
35
|
+
export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) {
|
|
36
36
|
const prelude =
|
|
37
37
|
`window.__IMPECCABLE_TOKEN__ = '${token}';\n` +
|
|
38
38
|
`window.__IMPECCABLE_PORT__ = ${port};\n` +
|
|
39
|
+
`window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` +
|
|
39
40
|
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
|
40
41
|
// builds its action picker from this instead of an inline copy.
|
|
41
42
|
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`;
|
|
@@ -95,11 +95,42 @@ export function substitutePropsWithExprs(markup, contract) {
|
|
|
95
95
|
return out;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function stripNamedBlocks(content, names) {
|
|
99
|
+
let out = String(content || '');
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
const opener = `<${name}`;
|
|
102
|
+
const closer = `</${name}`;
|
|
103
|
+
while (true) {
|
|
104
|
+
const lower = out.toLowerCase();
|
|
105
|
+
const start = lower.indexOf(opener);
|
|
106
|
+
if (start === -1) break;
|
|
107
|
+
const openEnd = lower.indexOf('>', start + opener.length);
|
|
108
|
+
const closeStart = openEnd === -1 ? -1 : lower.indexOf(closer, openEnd + 1);
|
|
109
|
+
const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + closer.length);
|
|
110
|
+
if (openEnd === -1 || closeStart === -1 || closeEnd === -1) break;
|
|
111
|
+
out = out.slice(0, start) + out.slice(closeEnd + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
while (true) {
|
|
115
|
+
const start = out.indexOf('<!--');
|
|
116
|
+
if (start === -1) break;
|
|
117
|
+
const normalEnd = out.indexOf('-->', start + 4);
|
|
118
|
+
const bangEnd = out.indexOf('--!>', start + 4);
|
|
119
|
+
const end = normalEnd === -1 ? bangEnd : bangEnd === -1 ? normalEnd : Math.min(normalEnd, bangEnd);
|
|
120
|
+
if (end === -1) break;
|
|
121
|
+
out = out.slice(0, start) + out.slice(end + (end === bangEnd ? 4 : 3));
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
export function parseSvelteComponentFile(content) {
|
|
99
127
|
const text = String(content || '');
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
128
|
+
const lower = text.toLowerCase();
|
|
129
|
+
const scriptStart = lower.indexOf('<script');
|
|
130
|
+
const scriptClose = scriptStart === -1 ? -1 : lower.indexOf('</script', scriptStart + 7);
|
|
131
|
+
const scriptEnd = scriptClose === -1 ? -1 : lower.indexOf('>', scriptClose + 8);
|
|
132
|
+
const withoutScript = scriptEnd === -1 ? text : text.slice(scriptEnd + 1);
|
|
133
|
+
const styleMatch = withoutScript.match(/<style\b[^>]*>[\s\S]*?<\/style\s*>/i);
|
|
103
134
|
const styleBlock = styleMatch ? styleMatch[0] : '';
|
|
104
135
|
const markup = styleMatch
|
|
105
136
|
? withoutScript.slice(0, styleMatch.index).trim()
|
|
@@ -107,7 +138,7 @@ export function parseSvelteComponentFile(content) {
|
|
|
107
138
|
const cssLines = styleBlock
|
|
108
139
|
? styleBlock
|
|
109
140
|
.replace(/^<style\b[^>]*>/i, '')
|
|
110
|
-
.replace(/<\/style\
|
|
141
|
+
.replace(/<\/style\s*>$/i, '')
|
|
111
142
|
.split('\n')
|
|
112
143
|
.map((line) => line.trimEnd())
|
|
113
144
|
: [];
|
|
@@ -290,7 +321,7 @@ function appendCssToSvelteStyle(lines, cssLines) {
|
|
|
290
321
|
|
|
291
322
|
function findLastStyleCloseLine(lines) {
|
|
292
323
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
293
|
-
if (/<\/style\
|
|
324
|
+
if (/<\/style\s*>/.test(lines[i])) return i;
|
|
294
325
|
}
|
|
295
326
|
return -1;
|
|
296
327
|
}
|
|
@@ -631,17 +662,8 @@ function inlineSvelteComponentInsertAccept({
|
|
|
631
662
|
};
|
|
632
663
|
}
|
|
633
664
|
|
|
634
|
-
function svelteMarkupHasVisibleContent(markup) {
|
|
635
|
-
|
|
636
|
-
let previous;
|
|
637
|
-
do {
|
|
638
|
-
previous = stripped;
|
|
639
|
-
stripped = stripped
|
|
640
|
-
.replace(/<script[\s\S]*?<\/script\b[^>]*>/gi, ' ')
|
|
641
|
-
.replace(/<style[\s\S]*?<\/style\b[^>]*>/gi, ' ')
|
|
642
|
-
.replace(/<!--[\s\S]*?-->/g, '');
|
|
643
|
-
} while (stripped !== previous);
|
|
644
|
-
const text = stripped
|
|
665
|
+
export function svelteMarkupHasVisibleContent(markup) {
|
|
666
|
+
const text = stripNamedBlocks(markup, ['script', 'style'])
|
|
645
667
|
.replace(/<[^>]+>/g, ' ')
|
|
646
668
|
.replace(/\s+/g, ' ')
|
|
647
669
|
.trim();
|
|
@@ -400,18 +400,12 @@ function readHtmlAttr(tag, name) {
|
|
|
400
400
|
return decodeHtmlAttr(match[2]);
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
const HTML_ATTR_ENTITIES = {
|
|
404
|
-
'"': '"',
|
|
405
|
-
'<': '<',
|
|
406
|
-
'>': '>',
|
|
407
|
-
'&': '&',
|
|
408
|
-
};
|
|
409
|
-
|
|
410
403
|
function decodeHtmlAttr(value) {
|
|
411
|
-
return String(value || '')
|
|
412
|
-
/&
|
|
413
|
-
(
|
|
414
|
-
|
|
404
|
+
return String(value || '')
|
|
405
|
+
.replace(/"/g, '"')
|
|
406
|
+
.replace(/</g, '<')
|
|
407
|
+
.replace(/>/g, '>')
|
|
408
|
+
.replace(/&/g, '&');
|
|
415
409
|
}
|
|
416
410
|
|
|
417
411
|
// ---------------------------------------------------------------------------
|
|
@@ -527,6 +521,26 @@ function hasVariantWrapperAttr(line, id) {
|
|
|
527
521
|
* - Same-line `<style>…</style>` blocks
|
|
528
522
|
* - Multi-line `<style>\n…\n</style>` blocks
|
|
529
523
|
*/
|
|
524
|
+
function removeCompleteStyleElements(value) {
|
|
525
|
+
let out = value;
|
|
526
|
+
while (true) {
|
|
527
|
+
const lower = out.toLowerCase();
|
|
528
|
+
const start = lower.indexOf('<style');
|
|
529
|
+
if (start === -1) break;
|
|
530
|
+
const openEnd = lower.indexOf('>', start + 6);
|
|
531
|
+
if (openEnd === -1) break;
|
|
532
|
+
if (lower.slice(start, openEnd).trimEnd().endsWith('/')) {
|
|
533
|
+
out = out.slice(0, start) + out.slice(openEnd + 1);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const closeStart = lower.indexOf('</style', openEnd + 1);
|
|
537
|
+
const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + 7);
|
|
538
|
+
if (closeStart === -1 || closeEnd === -1) break;
|
|
539
|
+
out = out.slice(0, start) + out.slice(closeEnd + 1);
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
|
|
530
544
|
function stripStyleAndJoin(lines, block) {
|
|
531
545
|
const out = [];
|
|
532
546
|
let inStyle = false;
|
|
@@ -536,13 +550,11 @@ function stripStyleAndJoin(lines, block) {
|
|
|
536
550
|
if (!inStyle) {
|
|
537
551
|
// Strip any complete <style> elements on this line (self-closed or
|
|
538
552
|
// same-line-closed), including their body content.
|
|
539
|
-
line = line
|
|
540
|
-
.replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/g, ' ')
|
|
541
|
-
.replace(/<style\b[^>]*\/\s*>/g, ' ');
|
|
553
|
+
line = removeCompleteStyleElements(line);
|
|
542
554
|
|
|
543
555
|
// If a <style> opener remains (multi-line body starts here), strip from
|
|
544
556
|
// the opener to end-of-line and flip into skip mode.
|
|
545
|
-
const openerIdx = line.
|
|
557
|
+
const openerIdx = line.toLowerCase().indexOf('<style');
|
|
546
558
|
if (openerIdx !== -1) {
|
|
547
559
|
line = line.slice(0, openerIdx);
|
|
548
560
|
inStyle = true;
|
|
@@ -550,10 +562,12 @@ function stripStyleAndJoin(lines, block) {
|
|
|
550
562
|
out.push(line);
|
|
551
563
|
} else {
|
|
552
564
|
// In multi-line style body; drop everything until we see </style>.
|
|
553
|
-
const closeIdx = line.
|
|
565
|
+
const closeIdx = line.toLowerCase().indexOf('</style');
|
|
554
566
|
if (closeIdx !== -1) {
|
|
567
|
+
const closeEnd = line.indexOf('>', closeIdx + 7);
|
|
568
|
+
if (closeEnd === -1) continue;
|
|
555
569
|
inStyle = false;
|
|
556
|
-
out.push(line.slice(
|
|
570
|
+
out.push(line.slice(closeEnd + 1));
|
|
557
571
|
}
|
|
558
572
|
// else: skip line entirely
|
|
559
573
|
}
|
|
@@ -643,7 +657,7 @@ function extractCss(lines, block, id) {
|
|
|
643
657
|
// Self-closing: nothing to carbonize.
|
|
644
658
|
if (/<style\b[^>]*\/\s*>/.test(line)) return null;
|
|
645
659
|
// Same-line open + close: extract inner text.
|
|
646
|
-
const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\
|
|
660
|
+
const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/);
|
|
647
661
|
if (sameLine) {
|
|
648
662
|
const inner = stripJsxTemplateWrap(sameLine[1]);
|
|
649
663
|
return inner.length > 0 ? inner.split('\n') : null;
|
|
@@ -656,7 +670,7 @@ function extractCss(lines, block, id) {
|
|
|
656
670
|
// Detect </style> anywhere on the line — JSX template-literal closes
|
|
657
671
|
// (`}</style>`) put the close mid-line, and we don't want to absorb the
|
|
658
672
|
// template-literal punctuation as CSS content.
|
|
659
|
-
const closeIdx = line.
|
|
673
|
+
const closeIdx = line.indexOf('</style>');
|
|
660
674
|
if (closeIdx !== -1) break;
|
|
661
675
|
content.push(line);
|
|
662
676
|
}
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
|
|
58
58
|
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
|
|
59
59
|
const PREFIX = 'impeccable-live';
|
|
60
|
+
const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable';
|
|
60
61
|
const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style';
|
|
61
62
|
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
|
|
62
63
|
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
|
|
@@ -5076,7 +5077,7 @@
|
|
|
5076
5077
|
}
|
|
5077
5078
|
|
|
5078
5079
|
function extractSvelteComponentStyle(source) {
|
|
5079
|
-
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\
|
|
5080
|
+
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
|
5080
5081
|
return match ? match[1].trim() : '';
|
|
5081
5082
|
}
|
|
5082
5083
|
|
|
@@ -5104,8 +5105,7 @@
|
|
|
5104
5105
|
}
|
|
5105
5106
|
|
|
5106
5107
|
function scopeCssToSveltePreview(css, sessionId) {
|
|
5107
|
-
const
|
|
5108
|
-
const prefix = '[data-impeccable-variants="' + escapedSessionId + '"] ';
|
|
5108
|
+
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"] ';
|
|
5109
5109
|
return scopeCssBlock(String(css || ''), prefix).trim();
|
|
5110
5110
|
}
|
|
5111
5111
|
|
|
@@ -5606,7 +5606,7 @@
|
|
|
5606
5606
|
if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
|
|
5607
5607
|
return String(block)
|
|
5608
5608
|
.replace(
|
|
5609
|
-
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style
|
|
5609
|
+
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
|
|
5610
5610
|
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
|
|
5611
5611
|
)
|
|
5612
5612
|
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
|
|
@@ -5637,7 +5637,7 @@
|
|
|
5637
5637
|
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
|
|
5638
5638
|
if (!out) return '';
|
|
5639
5639
|
if (out.startsWith('--')) return out;
|
|
5640
|
-
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase())
|
|
5640
|
+
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase());
|
|
5641
5641
|
}
|
|
5642
5642
|
|
|
5643
5643
|
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
|
|
@@ -6146,7 +6146,7 @@
|
|
|
6146
6146
|
switch (msg.type) {
|
|
6147
6147
|
case 'connected':
|
|
6148
6148
|
hasProjectContext = !!msg.hasProjectContext;
|
|
6149
|
-
if (!hasProjectContext) showToast(
|
|
6149
|
+
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
|
|
6150
6150
|
console.log('[impeccable] Live mode connected.');
|
|
6151
6151
|
syncAgentPollingUi(!!msg.agentPolling);
|
|
6152
6152
|
startAgentStatusPoll();
|
|
@@ -10539,7 +10539,7 @@ void main() {
|
|
|
10539
10539
|
if (designState.present === false) {
|
|
10540
10540
|
const empty = document.createElement('div');
|
|
10541
10541
|
empty.className = 'empty';
|
|
10542
|
-
empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code
|
|
10542
|
+
empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>${IMPECCABLE_COMMAND} document</code> in your terminal, then re-open this panel.`;
|
|
10543
10543
|
body.appendChild(empty);
|
|
10544
10544
|
return;
|
|
10545
10545
|
}
|
|
@@ -10569,7 +10569,7 @@ void main() {
|
|
|
10569
10569
|
box.className = 'stale';
|
|
10570
10570
|
box.innerHTML = `
|
|
10571
10571
|
<span class="stale-dot"></span>
|
|
10572
|
-
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code
|
|
10572
|
+
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>${IMPECCABLE_COMMAND} document</code> to refresh the sidecar.</span>
|
|
10573
10573
|
`;
|
|
10574
10574
|
return box;
|
|
10575
10575
|
}
|
|
@@ -10577,7 +10577,7 @@ void main() {
|
|
|
10577
10577
|
function renderParsedMdCta() {
|
|
10578
10578
|
const box = document.createElement('div');
|
|
10579
10579
|
box.className = 'parsed-md-cta';
|
|
10580
|
-
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code
|
|
10580
|
+
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>${IMPECCABLE_COMMAND} document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
|
|
10581
10581
|
return box;
|
|
10582
10582
|
}
|
|
10583
10583
|
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
getDesignSidecarPath,
|
|
37
37
|
getLiveDir,
|
|
38
38
|
getLiveAnnotationsDir,
|
|
39
|
+
IMPECCABLE_COMMAND_PREFIX,
|
|
39
40
|
readLiveServerInfo,
|
|
40
41
|
removeLiveServerInfo,
|
|
41
42
|
resolveDesignSidecarPath,
|
|
@@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
413
414
|
token: state.token,
|
|
414
415
|
port: state.port,
|
|
415
416
|
vocabulary: LIVE_COMMANDS,
|
|
417
|
+
commandPrefix: IMPECCABLE_COMMAND_PREFIX,
|
|
416
418
|
parts,
|
|
417
419
|
});
|
|
418
420
|
res.writeHead(200, {
|