mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7871a9e392 | ||
|
|
b3abdc30a6 | ||
|
|
56f44523f7 | ||
|
|
5d4418e2dc | ||
|
|
abba4012ff | ||
|
|
e0a9d8e7d9 | ||
|
|
fccd91c6ac | ||
|
|
e5abceedc4 | ||
|
|
c29f30fa34 | ||
|
|
6360b27823 | ||
|
|
a66aefba80 | ||
|
|
77dd327080 | ||
|
|
ff1f15c7ad | ||
|
|
d2a9efb90f | ||
|
|
16a218e632 | ||
|
|
7b94585653 | ||
|
|
478325a2dd | ||
|
|
8d62b135fe | ||
|
|
611147a333 | ||
|
|
7d5c60d291 | ||
|
|
1f2c3f9d6b | ||
|
|
f88b2837a7 | ||
|
|
cf8f295dc3 |
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `$impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"description": "Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.",
|
||||
"description": "Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -31,35 +31,35 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.34", "", { "dependencies": { "@ai-sdk/provider": "4.0.6", "@ai-sdk/provider-utils": "5.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-k3cj4AQ2calM6idUodma1cYlQuEeRxBjzyc2IBO+5kx+yAI8x0fku/Fq+DBKdOgSwNoBbv7lO1CsmmEkBBFbmw=="],
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.39", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JAMGtYeEuaBzqbsPO4fkho6vQyNoVhsHASM4o59wmJRU6Vh7prjOp490Kmc7YQTY+ioU1/xYzXvWOtxZBup0Xw=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.44", "", { "dependencies": { "@ai-sdk/provider": "4.0.6", "@ai-sdk/provider-utils": "5.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dQ/AHH1do1iEpWEXdAge37FtpXV17fsj7NkL9w+xuzVgvmI7+dQFeDerul+F2I5czmxe61Lq+TnUREjU92UXYg=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.52", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SXUM8jzzuTUJRq+EOgPd5to6DSx0EKslVn+IVZHbUEX6k/3vCPNrvjckbK26HnNxHU/STxm+zTSJteqrO+7Z0w=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@4.0.37", "", { "dependencies": { "@ai-sdk/provider": "4.0.6", "@ai-sdk/provider-utils": "5.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wtf81hl7lAy/cMREfwxeMRcfGl7WrSiZI5nTEDwHqH/dEWEM8lPQn13s9S97nbQn1DHNeyduljH4huCeILP/bw=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@4.0.44", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bmRTDg06jQD+eX8nf214pET9+Oe8O1+lUIRGbWsGXj9IN2UJkpl1O1x7cvtiboyTtKSLvSRdVtItUfSl8sQ2GA=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@4.0.34", "", { "dependencies": { "@ai-sdk/provider": "4.0.6", "@ai-sdk/provider-utils": "5.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DPGehPPwuTmnWxCDxeWZJgGLn7Lh+QfiiN20tvqnEQMUwCmuadZ3FLlRtTkTpeEa2l+Lsa/sWVkBwShWGjGmKA=="],
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@4.0.42", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZxDca6jJalYuXrIGVrw6dnkpz1Io9AWy+/b/wVWIbjigHCbd+zWLpPi8NnK0OFU+U3YCpP+KWfUvEnG5pFhltA=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@4.0.6", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-YYXjvs8F3q/BdEn9tBDoDuQACotfR7c5foGw/ADsM6iAVC1JabqEjQSkwHv/Kg2vNIr1c2AVHcQb+JYsYk76Qw=="],
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.23", "", { "dependencies": { "@ai-sdk/provider": "4.0.6", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mFvAAszenK8Xny3v+4Vntke5IcUkyzjss3gTBOxVWNrSIWmt/FhQ5gCgoJXW7IK11cklRaicMPwOXTnnBtaqFQ=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.224", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.224", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.224", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.224", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.224", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.224", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.224", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.224", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.224" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-a4N8RcNf+8zqiTJjsntzJuYy3z/+J9K06/kVkZefNmAI8Yp4TmTZ+tpxUpKY8zhS8VewAxKSl9h0o/u7hKIdnA=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.233", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.233", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.233", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.233", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.233", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.233", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.233", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.233", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.233" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-Dy+YqhggwtbezDy3Ap2pb1sK3bOqnI+sLNnsVjB3AUWvR0QlGnjjrjORXY03Y50I+B1eFRNEcYPAZKRYlCkSLQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.224", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3J2uqD6quEzxDVXhD+SJuvZzK4/h4ePS1/84FOaCRCn6A2pGai0EsFshKGA0Vb1i0sOjTyiFPaaZweZHFoFSFw=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.233", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4WDiBZgcrmvTDJjS8RNZwoxGgMz/0EpOM+sYa6EtyjwHTd6It1H/+k5zBckCmBajbgS5/ASCJqdwZzi7dwBl0Q=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.224", "", { "os": "darwin", "cpu": "x64" }, "sha512-T2dBna5wHyPVF14qAEV5KmQOuQAPm7hGSeeYDZQBaVRGXQZOG/npwgB08ORBsGiZczKcP+0vKNWddkIx+KNWqg=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.233", "", { "os": "darwin", "cpu": "x64" }, "sha512-RaaEfNrbqSh77H5NdVF9cJQ0xhAUO92aOv71LSKSdAYModMeUvJN0k22Q7gvmx0TlmqJ+aVyCG8J8gVfgSL9mg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.224", "", { "os": "linux", "cpu": "arm64" }, "sha512-5a6HwjeXV5RPrD+J7hoOYjmjfxNGwV8+GqhjlVL80w1A/scOeMQQHzb2xcw0R2RMWmIZmpJyM7DFTKNyzGZ9zw=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.233", "", { "os": "linux", "cpu": "arm64" }, "sha512-Az9HjQthYQqRjJCacBtDIAHX3TRGK9WlACNb/UOGAK3JndNzZMprM2mK/t6YmP2cRLJsGyorxL7HZmR9R9HYaw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.224", "", { "os": "linux", "cpu": "arm64" }, "sha512-hHHDFRwo8WXvx9f0V3W7qmQA1PJywKiQffUCN9Msz0MyCZrSgryrgGH8wr13kw5PlhAPkdjkbmS2YFFM8oG6EQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.233", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z3uZdzt6xgJ3f4NIgO6lzBYSELULKSq6AL4OsNLBzuaEpVW0iYs1kUCaD9rcMlMrf3cV+Dk/GA/lTCGMgbucjQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.224", "", { "os": "linux", "cpu": "x64" }, "sha512-UwpgLmjxBqNyJfcKmj5PXHlVpYklhjkl0P+61/AQywyT3n3IeH7p2uggSzbVNLKjGO9YOmtpEgpYTARS0axU4g=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.233", "", { "os": "linux", "cpu": "x64" }, "sha512-jpbhV+n9PnxLiyheQ/HjtHIg/E5/jVsk2Vdu132BSoL/3bsObSmMqKgsqoMutzwRZvtpqRs2RPVcjsC8G4A9Zw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.224", "", { "os": "linux", "cpu": "x64" }, "sha512-ewwIW8XorD5JiJCm9smoos0r3T1SuQwtYEfjVb5JmSy1LLfbvJPjL3ujgGVWnZil4E4svw7+3D5nSYCA9/DsLQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.233", "", { "os": "linux", "cpu": "x64" }, "sha512-kYBIAQCu2f1YITcGbpUN2jfrkAzs59TVAragAhE2z+GrkIcxcpZwmaRY6heMBtaSY8SuyrwgqbCW9hJALYFnEg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.224", "", { "os": "win32", "cpu": "arm64" }, "sha512-kjoao51rcpQYjco5qWAgrvSrXtKTvR7KHFSRAT/IFbnlGFK43oKuSEf/AAlE7RYF6vViSqXjArMQGEbl8vaWVw=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.233", "", { "os": "win32", "cpu": "arm64" }, "sha512-aO2MaNdmQofyPLKszE4s+Ope/sLJPeI/ZlGdCcjYp7qhji2hgZ4bRWWsOrx5eKjz0gFK5CFFltILkFcNcxCsVg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.224", "", { "os": "win32", "cpu": "x64" }, "sha512-qvNT/XBbEDrvYVhqUmsnpr3LUiwKzgsCkOUrNxxQWg0G27hiADXDMLrJ04z3xgoLayQMEuE3v4uGISXP+UHN5g=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.233", "", { "os": "win32", "cpu": "x64" }, "sha512-TcAYyWPXS5mREZGUksuCZsLIRQjbo/Vriur2PqIhAmgZ1oiqBZO27a90sX60EUczD7yV8wpwOVhVLhUxO0kAEg=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.115.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ=="],
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@puppeteer/browsers": ["@puppeteer/browsers@3.1.0", "", { "dependencies": { "modern-tar": "^0.7.6", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-RDLpio3fH/qrj5k4DVY6eyiN8tCS0Zovd/6jW//n605oeqkWcUjn+3k+9ZtZBnbwMpsu0F7xDIiKXvVmG5c5Bw=="],
|
||||
"@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="],
|
||||
|
||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
|
||||
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||
|
||||
"ai": ["ai@7.0.56", "", { "dependencies": { "@ai-sdk/gateway": "4.0.44", "@ai-sdk/provider": "4.0.6", "@ai-sdk/provider-utils": "5.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Lp7q9tZFl/TGOpBBNgt2wZ85SoCvJJVlHQksMkqrgiFUnIGOw+L2FWJ0gXuNEngLQTp9+t32k8QuIB7Jc5uKRA=="],
|
||||
"ai": ["ai@7.0.66", "", { "dependencies": { "@ai-sdk/gateway": "4.0.52", "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wBUyoCYF3GVr+62nelBgR8YbpTSsMZrzFyOOjiwijylNSM2TFCW35C+Pml2vc59/WLMpyhS/LWZ55M+B9DAcSg=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
|
||||
"devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
|
||||
|
||||
"devtools-protocol": ["devtools-protocol@0.0.1653615", "", {}, "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA=="],
|
||||
"devtools-protocol": ["devtools-protocol@0.0.1666840", "", {}, "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
|
||||
|
||||
@@ -345,7 +345,7 @@
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"modern-tar": ["modern-tar@0.7.6", "", {}, "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg=="],
|
||||
"modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
@@ -381,9 +381,9 @@
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"puppeteer": ["puppeteer@25.5.0", "", { "dependencies": { "@puppeteer/browsers": "3.1.0", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1653615", "lilconfig": "^3.1.3", "puppeteer-core": "25.5.0", "typed-query-selector": "^2.12.2" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" } }, "sha512-qpp73xblxNr+bF0nSXTodM3v+zcK5IPo/GkjLsdUqRf/qpLJp/1KxBUbstoMMnwnPw9xD6OMei8kmYf6CLWfGw=="],
|
||||
"puppeteer": ["puppeteer@25.8.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "lilconfig": "^3.1.3", "puppeteer-core": "25.8.0", "typed-query-selector": "^2.12.2" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" } }, "sha512-3gcUJ+Jfodb5zNa/lWLZukBUwYiRIwAc8WRICoqfi+ZYmNWqpsPFyanTU3Gw/lhgII9aotVbAmhT6/NKHWgyUA=="],
|
||||
|
||||
"puppeteer-core": ["puppeteer-core@25.5.0", "", { "dependencies": { "@puppeteer/browsers": "3.1.0", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1653615", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.1" } }, "sha512-XPNT0dQJtphqQ4I29zxlG4IIPbg1iEHAQKWuQgtMJGXjACV77pZSmJvDi51IIIfd+DTKICcopJwUx4upVQ4XbA=="],
|
||||
"puppeteer-core": ["puppeteer-core@25.8.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.1" } }, "sha512-LDOrawV8vfCVk+yLj2ozvajNP4Sv3OV9y3Tpiyy2g2Z+aQlbcozP6KJfI4iSBq7YQER+86ihEtPa5ioiZyWxMQ=="],
|
||||
|
||||
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
||||
|
||||
@@ -435,7 +435,7 @@
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"svelte": ["svelte@5.56.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg=="],
|
||||
"svelte": ["svelte@5.56.9", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="],
|
||||
|
||||
|
||||
@@ -699,7 +699,7 @@ function deduplicateProviders(root, providers, scope) {
|
||||
* SKILL.md, so script-only fixes and removed files are detected.
|
||||
* Returns true if every bundle skill matches the local copy.
|
||||
*/
|
||||
function isUpToDate(root, providers, bundleDir, scope) {
|
||||
function isUpToDate(root, providers, bundleDir, scope, agentScope = scope) {
|
||||
const unique = deduplicateProviders(root, providers, scope);
|
||||
if (unique.length === 0) return false;
|
||||
|
||||
@@ -724,6 +724,8 @@ function isUpToDate(root, providers, bundleDir, scope) {
|
||||
if (bundleHash !== localHash) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!providerAgentsUpToDate(bundleDir, root, provider, agentScope)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -745,7 +747,8 @@ async function check() {
|
||||
console.log('Checking for updates...\n');
|
||||
try {
|
||||
const bundleDir = await downloadAndExtractBundle();
|
||||
const upToDate = isUpToDate(root, providers, bundleDir);
|
||||
const agentScope = isHomeDir(root) ? 'user' : undefined;
|
||||
const upToDate = isUpToDate(root, providers, bundleDir, undefined, agentScope);
|
||||
rmSync(bundleDir, { recursive: true, force: true });
|
||||
|
||||
if (upToDate) {
|
||||
@@ -1251,7 +1254,9 @@ function copyProviderSkills(bundleDir, root, targets, { scope } = {}) {
|
||||
}
|
||||
|
||||
// Native subagent definitions that ship in the bundle next to a provider's
|
||||
// skills. GitHub Copilot's live at `.github/agents/impeccable-*.agent.md`:
|
||||
// skills. Claude Code's live at `.claude/agents/impeccable-*.md`; project
|
||||
// agents take precedence over user agents. GitHub Copilot's live at
|
||||
// `.github/agents/impeccable-*.agent.md`:
|
||||
// project installs commit them at `<repo>/.github/agents/`, user-level
|
||||
// installs go to `~/.copilot/agents/` (Copilot's user-scope dir, NOT
|
||||
// `~/.github/`). On a name conflict Copilot lets the user-level file shadow
|
||||
@@ -1261,6 +1266,11 @@ function copyProviderSkills(bundleDir, root, targets, { scope } = {}) {
|
||||
// `~/.cursor/agents/`; project agents take precedence there, so no shadow
|
||||
// warning is needed.
|
||||
const PROVIDER_AGENT_ARTIFACTS = {
|
||||
'.claude': {
|
||||
ext: '.md',
|
||||
userDir: home => join(home, '.claude', 'agents'),
|
||||
userShadowsProject: false,
|
||||
},
|
||||
'.github': {
|
||||
ext: '.agent.md',
|
||||
userDir: home => join(home, '.copilot', 'agents'),
|
||||
@@ -1273,6 +1283,23 @@ const PROVIDER_AGENT_ARTIFACTS = {
|
||||
},
|
||||
};
|
||||
|
||||
function providerAgentsUpToDate(bundleDir, root, provider, scope) {
|
||||
const artifact = PROVIDER_AGENT_ARTIFACTS[provider];
|
||||
if (!artifact) return true;
|
||||
const srcDir = join(bundleDir, provider, 'agents');
|
||||
if (!existsSync(srcDir)) return true;
|
||||
|
||||
const destDir = scope === 'user'
|
||||
? artifact.userDir(root)
|
||||
: join(root, provider, 'agents');
|
||||
const agentFiles = readdirSync(srcDir).filter(name => name.endsWith(artifact.ext));
|
||||
return agentFiles.every(name => {
|
||||
const localPath = join(destDir, name);
|
||||
return existsSync(localPath)
|
||||
&& hashSkillFile(join(srcDir, name)) === hashSkillFile(localPath);
|
||||
});
|
||||
}
|
||||
|
||||
function copyProviderAgents(bundleDir, root, providers, { scope, home = homedir() } = {}) {
|
||||
const targets = Array.isArray(providers) ? providers : [providers];
|
||||
const results = [];
|
||||
@@ -2073,7 +2100,9 @@ function resolveUpdateTarget({ projectRoot, home, explicitScope }) {
|
||||
const homeRooted = isHomeDir(projectRoot);
|
||||
if (homeRooted && !explicitScope) {
|
||||
const providers = findInstalledProviders(home);
|
||||
return providers.length ? { root: home, scope: undefined, providers, scopeLabel: 'user level' } : null;
|
||||
return providers.length
|
||||
? { root: home, scope: undefined, agentScope: 'user', providers, scopeLabel: 'user level' }
|
||||
: null;
|
||||
}
|
||||
|
||||
const projectProviders = homeRooted ? [] : findImpeccableProviders(projectRoot, 'project');
|
||||
@@ -2204,7 +2233,7 @@ async function update(flags = []) {
|
||||
: { root: projectRoot, scope: 'project', providers: target.projectProviders, scopeLabel: 'this project' };
|
||||
}
|
||||
|
||||
const { root, scope } = target;
|
||||
const { root, scope, agentScope = scope } = target;
|
||||
console.log(`Updating the ${target.scopeLabel} install: ${formatPathForDisplay(root)} (${target.providers.join(', ')})`);
|
||||
const providers = target.providers;
|
||||
const linkedProviders = findLinkedProviders(root, providers, scope);
|
||||
@@ -2228,7 +2257,7 @@ async function update(flags = []) {
|
||||
}
|
||||
|
||||
// Compare local vs remote -- skip if already up to date
|
||||
if (isUpToDate(root, copyProviders, tmpDir, scope)) {
|
||||
if (isUpToDate(root, copyProviders, tmpDir, scope, agentScope)) {
|
||||
try {
|
||||
const wantHooks = installHooks && await decideHookInstall(root, copyProviders, { yes });
|
||||
const hookTargets = wantHooks ? copyProviderHooks(tmpDir, root, copyProviders, { force }) : [];
|
||||
@@ -2264,7 +2293,7 @@ async function update(flags = []) {
|
||||
if (migrated > 0) console.log('Migrated a prefixed install back to /impeccable (the i- prefix is no longer used).');
|
||||
|
||||
const updated = refreshProviderSkills(tmpDir, root, copyProviders, scope);
|
||||
reportProviderAgents(copyProviderAgents(tmpDir, root, copyProviders, { scope }));
|
||||
reportProviderAgents(copyProviderAgents(tmpDir, root, copyProviders, { scope: agentScope }));
|
||||
const wantHooks = installHooks && await decideHookInstall(root, providers, { yes });
|
||||
const hookTargets = wantHooks ? copyProviderHooks(tmpDir, root, providers, { force }) : [];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -137,9 +137,9 @@ const GROK_PROJECT_HOOK = '.grok/skills/impeccable/scripts/hook.mjs';
|
||||
|
||||
export function buildClaudeSettingsManifest() {
|
||||
return {
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write|MultiEdit',
|
||||
'Edit|Write',
|
||||
CLAUDE_PROJECT_HOOK,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
@@ -155,7 +155,7 @@ export function buildClaudeSettingsManifest() {
|
||||
export function buildClaudePluginHooksManifest() {
|
||||
return {
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write|MultiEdit',
|
||||
'Edit|Write',
|
||||
CLAUDE_PLUGIN_HOOK,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
|
||||
+107
-151
@@ -12,6 +12,8 @@ import { homedir, tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { parseArgs } from './lib/cli-args.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const prRoot = resolve(__dirname, '..');
|
||||
const defaultBundle = join(prRoot, 'dist', 'universal.zip');
|
||||
@@ -32,31 +34,60 @@ if (args.help || args.h || !args.repo) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const targetRepo = resolve(args.repo);
|
||||
const bundlePath = resolve(args.bundle || defaultBundle);
|
||||
const selectedProviders = (args.providers || defaultProviders.join(','))
|
||||
const targetRepo = resolve(legacySmokeArg(args.repo));
|
||||
const bundlePath = resolve(legacySmokeArg(args.bundle) || defaultBundle);
|
||||
const selectedProviders = (legacySmokeArg(args.providers) || defaultProviders.join(','))
|
||||
.split(',')
|
||||
.map((provider) => provider.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const smokeDir = join(targetRepo, '.impeccable', 'provider-smoke');
|
||||
const summaryPath = join(smokeDir, 'summary.json');
|
||||
const smokeFiles = {
|
||||
direct: 'src/__impeccable_provider_smoke_direct.html',
|
||||
claude: 'src/__impeccable_provider_smoke_claude.html',
|
||||
codex: 'src/__impeccable_provider_smoke_codex.html',
|
||||
cursor: 'src/__impeccable_provider_smoke_cursor.html',
|
||||
confirmedClaude: 'src/__impeccable_provider_smoke_confirmed_claude.html',
|
||||
confirmedCodex: 'src/__impeccable_provider_smoke_confirmed_codex.html',
|
||||
confirmedCursor: 'src/__impeccable_provider_smoke_confirmed_cursor.html',
|
||||
agentChoiceClaude: 'src/__impeccable_provider_smoke_font_choice_claude.html',
|
||||
agentChoiceCodex: 'src/__impeccable_provider_smoke_font_choice_codex.html',
|
||||
agentChoiceCursor: 'src/__impeccable_provider_smoke_font_choice_cursor.html',
|
||||
const directSmokeFile = 'src/__impeccable_provider_smoke_direct.html';
|
||||
const providerSmoke = {
|
||||
claude: {
|
||||
fixture: 'src/__impeccable_provider_smoke_claude.html',
|
||||
confirmedFixture: 'src/__impeccable_provider_smoke_confirmed_claude.html',
|
||||
agentChoiceFixture: 'src/__impeccable_provider_smoke_font_choice_claude.html',
|
||||
admin: '.claude/skills/impeccable/scripts/hook-admin.mjs',
|
||||
hook: '.claude/skills/impeccable/scripts/hook.mjs',
|
||||
event: (file) => postToolUseEvent('confirmed-claude', file, 'Edit'),
|
||||
},
|
||||
codex: {
|
||||
fixture: 'src/__impeccable_provider_smoke_codex.html',
|
||||
confirmedFixture: 'src/__impeccable_provider_smoke_confirmed_codex.html',
|
||||
agentChoiceFixture: 'src/__impeccable_provider_smoke_font_choice_codex.html',
|
||||
admin: '.agents/skills/impeccable/scripts/hook-admin.mjs',
|
||||
hook: '.agents/skills/impeccable/scripts/hook.mjs',
|
||||
event: (file) => postToolUseEvent('confirmed-codex', file, 'apply_patch'),
|
||||
},
|
||||
cursor: {
|
||||
fixture: 'src/__impeccable_provider_smoke_cursor.html',
|
||||
confirmedFixture: 'src/__impeccable_provider_smoke_confirmed_cursor.html',
|
||||
agentChoiceFixture: 'src/__impeccable_provider_smoke_font_choice_cursor.html',
|
||||
admin: '.cursor/skills/impeccable/scripts/hook-admin.mjs',
|
||||
hook: '.cursor/skills/impeccable/scripts/hook-before-edit.mjs',
|
||||
event: (file) => ({
|
||||
hook_event_name: 'preToolUse',
|
||||
cwd: targetRepo,
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: file,
|
||||
content: readFileSync(file, 'utf8'),
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const results = [];
|
||||
const hookConfigFiles = ['.impeccable/config.json', '.impeccable/config.local.json'];
|
||||
const originalHookConfigFiles = new Map();
|
||||
|
||||
function legacySmokeArg(value) {
|
||||
// The retired local parser represented a bare flag as the string "true".
|
||||
// Preserve that CLI/error behavior while sharing the repository parser.
|
||||
return value === true ? 'true' : value;
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
restoreHookConfigFiles();
|
||||
if (!results.some((result) => !result.pass)) {
|
||||
@@ -109,21 +140,6 @@ async function checked(name, classification, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith('--')) continue;
|
||||
const eq = arg.indexOf('=');
|
||||
if (eq !== -1) {
|
||||
out[arg.slice(2, eq)] = arg.slice(eq + 1);
|
||||
} else {
|
||||
out[arg.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : 'true';
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function assertPath(path, label) {
|
||||
if (!existsSync(path)) throw new Error(`${label} does not exist: ${path}`);
|
||||
}
|
||||
@@ -466,7 +482,7 @@ function assertNoPluginInstall() {
|
||||
|
||||
function runDirectContractChecks() {
|
||||
clearRuntimeState();
|
||||
const file = writeBadFixture(smokeFiles.direct);
|
||||
const file = writeBadFixture(directSmokeFile);
|
||||
const env = { IMPECCABLE_HOOK_LOG: join(smokeDir, 'direct.ndjson') };
|
||||
const claude = run('node', ['.claude/skills/impeccable/scripts/hook.mjs'], {
|
||||
cwd: targetRepo,
|
||||
@@ -495,7 +511,7 @@ function runDirectContractChecks() {
|
||||
cwd: targetRepo,
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: join(targetRepo, smokeFiles.direct),
|
||||
file_path: join(targetRepo, directSmokeFile),
|
||||
content: badFixtureContent(),
|
||||
},
|
||||
}),
|
||||
@@ -515,7 +531,7 @@ function runConfirmedExceptionPersistenceChecks() {
|
||||
|
||||
function runConfirmedExceptionForProvider(provider) {
|
||||
clearRuntimeState();
|
||||
const rel = confirmedSmokeFile(provider);
|
||||
const rel = providerSmoke[provider].confirmedFixture;
|
||||
const file = writeConfirmedFixture(rel);
|
||||
const beforeLog = `${provider}-confirmed-before.ndjson`;
|
||||
const afterLog = `${provider}-confirmed-after.ndjson`;
|
||||
@@ -528,7 +544,7 @@ function runConfirmedExceptionForProvider(provider) {
|
||||
assertNoSpecificFontIgnoreConfig(provider);
|
||||
|
||||
run('node', [
|
||||
providerAdminScript(provider),
|
||||
providerSmoke[provider].admin,
|
||||
'ignore-value',
|
||||
'overused-font',
|
||||
'Roboto',
|
||||
@@ -580,7 +596,7 @@ function runAgentChosenFontExceptionChecks() {
|
||||
|
||||
function runAgentChosenFontExceptionForProvider(provider) {
|
||||
clearRuntimeState();
|
||||
const rel = agentChoiceSmokeFile(provider);
|
||||
const rel = providerSmoke[provider].agentChoiceFixture;
|
||||
const file = writeConfirmedFixture(rel);
|
||||
const beforeLog = `${provider}-agent-choice-before.ndjson`;
|
||||
const afterLog = `${provider}-agent-choice-after.ndjson`;
|
||||
@@ -612,27 +628,40 @@ function runAgentChosenFontExceptionForProvider(provider) {
|
||||
}
|
||||
|
||||
function runProviderAgentFontException(provider, rel) {
|
||||
const prompt = fontExceptionPrompt(provider, rel);
|
||||
runProviderAgent(provider, fontExceptionPrompt(provider, rel), {
|
||||
logName: `${provider}-agent-choice.log`,
|
||||
claudeDebugLog: 'claude-agent-choice-debug.log',
|
||||
});
|
||||
}
|
||||
|
||||
function runProviderAgent(provider, prompt, {
|
||||
logName,
|
||||
env = {},
|
||||
claudeDebugLog,
|
||||
claudeTools = 'Read,Bash',
|
||||
claudeAllowedTools = 'Read Bash',
|
||||
cursorReady = false,
|
||||
} = {}) {
|
||||
if (provider === 'claude') {
|
||||
run('claude', [
|
||||
return run('claude', [
|
||||
'-p',
|
||||
'--setting-sources', 'project',
|
||||
'--permission-mode', 'acceptEdits',
|
||||
'--tools', 'Read,Bash',
|
||||
'--allowedTools', 'Read Bash',
|
||||
'--tools', claudeTools,
|
||||
'--allowedTools', claudeAllowedTools,
|
||||
'--debug', 'hooks',
|
||||
'--debug-file', join(smokeDir, 'claude-agent-choice-debug.log'),
|
||||
'--debug-file', join(smokeDir, claudeDebugLog),
|
||||
prompt,
|
||||
], {
|
||||
cwd: targetRepo,
|
||||
logName: 'claude-agent-choice.log',
|
||||
env,
|
||||
logName,
|
||||
timeoutMs: 10 * 60 * 1000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'codex') {
|
||||
run('codex', [
|
||||
return run('codex', [
|
||||
'exec',
|
||||
'-C', targetRepo,
|
||||
'--dangerously-bypass-hook-trust',
|
||||
@@ -641,14 +670,14 @@ function runProviderAgentFontException(provider, rel) {
|
||||
prompt,
|
||||
], {
|
||||
cwd: targetRepo,
|
||||
logName: 'codex-agent-choice.log',
|
||||
env,
|
||||
logName,
|
||||
timeoutMs: 10 * 60 * 1000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'cursor') {
|
||||
ensureCursorAgent();
|
||||
if (!cursorReady) ensureCursorAgent();
|
||||
const res = run('agent', [
|
||||
'-p',
|
||||
'--force',
|
||||
@@ -658,7 +687,8 @@ function runProviderAgentFontException(provider, rel) {
|
||||
prompt,
|
||||
], {
|
||||
cwd: targetRepo,
|
||||
logName: 'cursor-agent-choice.log',
|
||||
env,
|
||||
logName,
|
||||
timeoutMs: 10 * 60 * 1000,
|
||||
allowFailure: true,
|
||||
});
|
||||
@@ -671,10 +701,10 @@ function runProviderAgentFontException(provider, rel) {
|
||||
}
|
||||
throw new Error(res.error ? `agent failed: ${res.error.message}` : `agent exited ${res.status}`);
|
||||
}
|
||||
return;
|
||||
return res;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported agent-choice provider: ${provider}`);
|
||||
throw new Error(`Unsupported provider agent: ${provider}`);
|
||||
}
|
||||
|
||||
function assertSpecificFontIgnoreConfig(provider, config) {
|
||||
@@ -713,84 +743,29 @@ function readSharedHookConfig() {
|
||||
}
|
||||
|
||||
function runInstalledProviderHook(provider, file, logName) {
|
||||
const smoke = providerSmoke[provider];
|
||||
const env = { IMPECCABLE_HOOK_LOG: join(smokeDir, logName) };
|
||||
if (provider === 'claude') {
|
||||
return run('node', ['.claude/skills/impeccable/scripts/hook.mjs'], {
|
||||
cwd: targetRepo,
|
||||
env,
|
||||
logName: `direct-${provider}-confirmed-${logName.replace(/\.ndjson$/, '.log')}`,
|
||||
input: JSON.stringify(postToolUseEvent(`confirmed-${provider}`, file, 'Edit')),
|
||||
});
|
||||
}
|
||||
if (provider === 'codex') {
|
||||
return run('node', ['.agents/skills/impeccable/scripts/hook.mjs'], {
|
||||
cwd: targetRepo,
|
||||
env,
|
||||
logName: `direct-${provider}-confirmed-${logName.replace(/\.ndjson$/, '.log')}`,
|
||||
input: JSON.stringify(postToolUseEvent(`confirmed-${provider}`, file, 'apply_patch')),
|
||||
});
|
||||
}
|
||||
if (provider === 'cursor') {
|
||||
return run('node', ['.cursor/skills/impeccable/scripts/hook-before-edit.mjs'], {
|
||||
cwd: targetRepo,
|
||||
env,
|
||||
logName: `direct-${provider}-confirmed-${logName.replace(/\.ndjson$/, '.log')}`,
|
||||
input: JSON.stringify({
|
||||
hook_event_name: 'preToolUse',
|
||||
cwd: targetRepo,
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: file,
|
||||
content: readFileSync(file, 'utf8'),
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
throw new Error(`Unsupported confirmed exception provider: ${provider}`);
|
||||
}
|
||||
|
||||
function confirmedSmokeFile(provider) {
|
||||
if (provider === 'claude') return smokeFiles.confirmedClaude;
|
||||
if (provider === 'codex') return smokeFiles.confirmedCodex;
|
||||
if (provider === 'cursor') return smokeFiles.confirmedCursor;
|
||||
throw new Error(`Unsupported confirmed exception provider: ${provider}`);
|
||||
}
|
||||
|
||||
function agentChoiceSmokeFile(provider) {
|
||||
if (provider === 'claude') return smokeFiles.agentChoiceClaude;
|
||||
if (provider === 'codex') return smokeFiles.agentChoiceCodex;
|
||||
if (provider === 'cursor') return smokeFiles.agentChoiceCursor;
|
||||
throw new Error(`Unsupported agent-choice provider: ${provider}`);
|
||||
}
|
||||
|
||||
function providerAdminScript(provider) {
|
||||
if (provider === 'claude') return '.claude/skills/impeccable/scripts/hook-admin.mjs';
|
||||
if (provider === 'codex') return '.agents/skills/impeccable/scripts/hook-admin.mjs';
|
||||
if (provider === 'cursor') return '.cursor/skills/impeccable/scripts/hook-admin.mjs';
|
||||
throw new Error(`Unsupported admin provider: ${provider}`);
|
||||
return run('node', [smoke.hook], {
|
||||
cwd: targetRepo,
|
||||
env,
|
||||
logName: `direct-${provider}-confirmed-${logName.replace(/\.ndjson$/, '.log')}`,
|
||||
input: JSON.stringify(smoke.event(file)),
|
||||
});
|
||||
}
|
||||
|
||||
function runClaudeProviderSmoke() {
|
||||
clearRuntimeState();
|
||||
const env = { IMPECCABLE_HOOK_LOG: join(smokeDir, 'claude.ndjson') };
|
||||
const prompt = providerPrompt(smokeFiles.claude);
|
||||
const res = run('claude', [
|
||||
'-p',
|
||||
'--setting-sources', 'project',
|
||||
'--permission-mode', 'acceptEdits',
|
||||
'--tools', 'Read,Write,Edit',
|
||||
'--allowedTools', 'Read Write Edit',
|
||||
'--debug', 'hooks',
|
||||
'--debug-file', join(smokeDir, 'claude-debug.log'),
|
||||
prompt,
|
||||
], {
|
||||
cwd: targetRepo,
|
||||
const prompt = providerPrompt(providerSmoke.claude.fixture);
|
||||
const res = runProviderAgent('claude', prompt, {
|
||||
env,
|
||||
logName: 'claude-provider.log',
|
||||
timeoutMs: 10 * 60 * 1000,
|
||||
claudeDebugLog: 'claude-debug.log',
|
||||
claudeTools: 'Read,Write,Edit',
|
||||
claudeAllowedTools: 'Read Write Edit',
|
||||
});
|
||||
const evidence = `${res.stdout}\n${res.stderr}\n${readMaybe(join(smokeDir, 'claude.ndjson'))}\n${readMaybe(join(smokeDir, 'claude-debug.log'))}`;
|
||||
requireFile(smokeFiles.claude, 'Claude provider fixture');
|
||||
requireFile(providerSmoke.claude.fixture, 'Claude provider fixture');
|
||||
requireFinding('Claude provider hook', evidence);
|
||||
if (!/PostToolUse|hook/i.test(evidence)) throw new Error('Claude provider evidence lacks hook/PostToolUse marker');
|
||||
record('claude provider', true, 'Claude edit triggered PostToolUse hook and side-tab detection');
|
||||
@@ -799,23 +774,14 @@ function runClaudeProviderSmoke() {
|
||||
function runCodexProviderSmoke() {
|
||||
clearRuntimeState();
|
||||
const env = { IMPECCABLE_HOOK_LOG: join(smokeDir, 'codex.ndjson') };
|
||||
const prompt = `Use apply_patch to ${providerPrompt(smokeFiles.codex)}`;
|
||||
const res = run('codex', [
|
||||
'exec',
|
||||
'-C', targetRepo,
|
||||
'--dangerously-bypass-hook-trust',
|
||||
'--dangerously-bypass-approvals-and-sandbox',
|
||||
'--json',
|
||||
prompt,
|
||||
], {
|
||||
cwd: targetRepo,
|
||||
const prompt = `Use apply_patch to ${providerPrompt(providerSmoke.codex.fixture)}`;
|
||||
const res = runProviderAgent('codex', prompt, {
|
||||
env,
|
||||
logName: 'codex-provider.log',
|
||||
timeoutMs: 10 * 60 * 1000,
|
||||
});
|
||||
const evidence = `${res.stdout}\n${res.stderr}\n${readMaybe(join(smokeDir, 'codex.ndjson'))}`;
|
||||
const cacheEvidence = `${readMaybe(join(targetRepo, '.impeccable', 'hook.cache.json'))}\n${readMaybe(join(targetRepo, '.impeccable', 'hook.pending.json'))}`;
|
||||
requireFile(smokeFiles.codex, 'Codex provider fixture');
|
||||
requireFile(providerSmoke.codex.fixture, 'Codex provider fixture');
|
||||
requireFinding('Codex provider hook', `${evidence}\n${cacheEvidence}`);
|
||||
record('codex provider', true, 'Codex apply_patch triggered project hook and side-tab detection');
|
||||
}
|
||||
@@ -824,37 +790,19 @@ function runCursorProviderSmoke() {
|
||||
ensureCursorAgent();
|
||||
clearRuntimeState();
|
||||
const env = { IMPECCABLE_HOOK_LOG: join(smokeDir, 'cursor.ndjson') };
|
||||
const prompt = providerPrompt(smokeFiles.cursor);
|
||||
const res = run('agent', [
|
||||
'-p',
|
||||
'--force',
|
||||
'--trust',
|
||||
'--workspace', targetRepo,
|
||||
'--output-format', 'stream-json',
|
||||
prompt,
|
||||
], {
|
||||
cwd: targetRepo,
|
||||
const prompt = providerPrompt(providerSmoke.cursor.fixture);
|
||||
const res = runProviderAgent('cursor', prompt, {
|
||||
env,
|
||||
logName: 'cursor-provider.log',
|
||||
timeoutMs: 10 * 60 * 1000,
|
||||
allowFailure: true,
|
||||
cursorReady: true,
|
||||
});
|
||||
if (res.error || res.status !== 0) {
|
||||
const output = `${res.stdout}\n${res.stderr}\n${res.error?.message || ''}`;
|
||||
if (/Authentication required|agent login|CURSOR_API_KEY/i.test(output)) {
|
||||
const err = new Error('Cursor CLI authentication required. Run `agent login` or set CURSOR_API_KEY, then rerun `bun run smoke:hooks -- --providers=cursor`.');
|
||||
err.classification = 'cursor auth required';
|
||||
throw err;
|
||||
}
|
||||
throw new Error(res.error ? `agent failed: ${res.error.message}` : `agent exited ${res.status}`);
|
||||
}
|
||||
const evidence = `${res.stdout}\n${res.stderr}\n${readMaybe(join(smokeDir, 'cursor.ndjson'))}\n${readMaybe(join(targetRepo, '.impeccable', 'hook.pending.json'))}\n${readMaybe(join(targetRepo, '.impeccable', 'hook.cache.json'))}`;
|
||||
requireFinding('Cursor provider hook', evidence);
|
||||
const auditEvents = readAuditEvents(join(smokeDir, 'cursor.ndjson'));
|
||||
if (!auditEvents.some((event) => event.event === 'preToolUse' && event.blocked === true)) {
|
||||
throw new Error('Cursor provider evidence lacks a preToolUse audit entry with blocked=true');
|
||||
}
|
||||
const fixturePath = join(targetRepo, smokeFiles.cursor);
|
||||
const fixturePath = join(targetRepo, providerSmoke.cursor.fixture);
|
||||
const intentionalIgnore = auditEvents.some((event) =>
|
||||
event.event === 'preToolUse'
|
||||
&& event.file === fixturePath
|
||||
@@ -1017,7 +965,15 @@ function cleanSmokeArtifacts() {
|
||||
}
|
||||
|
||||
function cleanSmokeFiles() {
|
||||
for (const rel of Object.values(smokeFiles)) {
|
||||
const files = [
|
||||
directSmokeFile,
|
||||
...Object.values(providerSmoke).flatMap(({ fixture, confirmedFixture, agentChoiceFixture }) => [
|
||||
fixture,
|
||||
confirmedFixture,
|
||||
agentChoiceFixture,
|
||||
]),
|
||||
];
|
||||
for (const rel of files) {
|
||||
rmSync(join(targetRepo, rel), { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `{{command_prefix}}impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -21,22 +21,24 @@ import zlib from 'node:zlib';
|
||||
const KEYWORD = 'impeccable:prompt';
|
||||
const args = process.argv.slice(2);
|
||||
const file = args.find(a => !a.startsWith('--'));
|
||||
const readMode = args.includes('--read');
|
||||
const scanMode = args.includes('--scan');
|
||||
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
|
||||
|
||||
function promptOf(imagePath) {
|
||||
const b = fs.readFileSync(imagePath);
|
||||
let prompt = null;
|
||||
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
|
||||
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
|
||||
function imageType(buffer) {
|
||||
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
|
||||
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
|
||||
const type = imageType(buffer);
|
||||
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
|
||||
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (scanMode) {
|
||||
if (args.includes('--scan')) {
|
||||
const targets = args.filter(a => !a.startsWith('--'));
|
||||
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
|
||||
const RASTER = /\.(png|jpe?g|webp)$/i;
|
||||
@@ -59,7 +61,7 @@ if (scanMode) {
|
||||
}
|
||||
let missing = 0;
|
||||
for (const raster of rasters) {
|
||||
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
|
||||
}
|
||||
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
|
||||
process.exit(missing > 0 ? 3 : 0);
|
||||
@@ -68,8 +70,7 @@ if (scanMode) {
|
||||
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
|
||||
|
||||
const buf = fs.readFileSync(file);
|
||||
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
|
||||
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
|
||||
const type = imageType(buf);
|
||||
|
||||
const crcTable = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
@@ -87,22 +88,26 @@ function pngChunk(type, data) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function readPngText(b) {
|
||||
let off = 8;
|
||||
while (off + 12 <= b.length) {
|
||||
const len = b.readUInt32BE(off);
|
||||
const type = b.toString('ascii', off + 4, off + 8);
|
||||
if (type === 'tEXt' || type === 'zTXt') {
|
||||
const data = b.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
|
||||
if (type === 'tEXt') return data.toString('utf8', nul + 1);
|
||||
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
function parsePng(buffer) {
|
||||
const chunks = [];
|
||||
let prompt = null;
|
||||
let offset = 8;
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
const nul = data.indexOf(0);
|
||||
const promptChunk = (type === 'tEXt' || type === 'zTXt')
|
||||
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (prompt == null && promptChunk) {
|
||||
prompt = type === 'tEXt'
|
||||
? data.toString('utf8', nul + 1)
|
||||
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
|
||||
}
|
||||
off += 12 + len;
|
||||
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
|
||||
offset += 12 + length;
|
||||
}
|
||||
return null;
|
||||
return { chunks, prompt };
|
||||
}
|
||||
|
||||
function readJpegCom(b) {
|
||||
@@ -121,48 +126,34 @@ function readJpegCom(b) {
|
||||
}
|
||||
|
||||
const sidecar = `${file}.json`;
|
||||
if (readMode) {
|
||||
let prompt = null;
|
||||
if (isPng) prompt = readPngText(buf);
|
||||
else if (isJpeg) prompt = readJpegCom(buf);
|
||||
if (prompt == null && fs.existsSync(sidecar)) {
|
||||
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
|
||||
}
|
||||
if (args.includes('--read')) {
|
||||
const prompt = readPrompt(file, buf);
|
||||
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
|
||||
const promptFile = argOf('--prompt-file');
|
||||
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
|
||||
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
|
||||
|
||||
if (isPng) {
|
||||
if (type === 'png') {
|
||||
// Insert (or replace) our tEXt chunk immediately before IEND.
|
||||
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
|
||||
const { chunks, prompt: existingPrompt } = parsePng(buf);
|
||||
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
|
||||
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
|
||||
// Drop any existing chunk with our keyword to keep embedding idempotent.
|
||||
let body = buf.subarray(8, iend);
|
||||
const existing = readPngText(buf);
|
||||
if (existing != null) {
|
||||
const parts = [];
|
||||
let off = 8;
|
||||
while (off + 12 <= buf.length && off < iend + 12) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.toString('ascii', off + 4, off + 8);
|
||||
const chunk = buf.subarray(off, off + 12 + len);
|
||||
const data = buf.subarray(off + 8, off + 8 + len);
|
||||
const nul = data.indexOf(0);
|
||||
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
|
||||
if (!ours && type !== 'IEND') parts.push(chunk);
|
||||
off += 12 + len;
|
||||
}
|
||||
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
|
||||
} else {
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
|
||||
}
|
||||
const replacing = existingPrompt != null;
|
||||
const body = replacing
|
||||
? Buffer.concat(chunks
|
||||
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
|
||||
.map((chunk) => chunk.bytes))
|
||||
: buf.subarray(8, iend);
|
||||
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
|
||||
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
|
||||
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
|
||||
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
|
||||
} else if (isJpeg) {
|
||||
} else if (type === 'jpeg') {
|
||||
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
|
||||
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
|
||||
const com = Buffer.alloc(4 + seg.length);
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -196,9 +196,6 @@ function parseScalar(raw) {
|
||||
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const OKLCH_RE = /oklch\([^)]+\)/gi;
|
||||
const RGBA_RE = /rgba?\([^)]+\)/gi;
|
||||
const BOX_SHADOW_RE = /(?:box-shadow:\s*)?((?:-?\d[\w\d\s\-.,/()#%]*)+)/;
|
||||
const NAMED_RULE_RE = /\*\*(The [^*]+?Rule)\.\*\*\s*(.+)/;
|
||||
|
||||
// ---------- Section splitting ----------
|
||||
|
||||
@@ -550,36 +547,6 @@ function detectFormat(v) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function scanInlineColors(lines) {
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '');
|
||||
const color = parseColorBullet(trimmed);
|
||||
if (color) out.push(color);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStitchInlineGroups(lines) {
|
||||
// Stitch writes: `* **Primary (`#00478d` to `#005eb8`):** Use for "..."`
|
||||
// Each bullet IS its own role. Group them under the spoken role name.
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
if (!/^\s*[-*]\s/.test(line)) continue;
|
||||
const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
|
||||
const m = trimmed.match(
|
||||
/^\*\*([A-Z][a-zA-Z]+)\s*\(([^)]+)\):\*\*\s*(.*)$/
|
||||
);
|
||||
if (m) {
|
||||
const role = m[1];
|
||||
const color = buildColor(role, m[2], m[3]);
|
||||
out.push({ role, colors: [color] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTypography(section) {
|
||||
if (!section) return null;
|
||||
const text = section.lines.join('\n');
|
||||
|
||||
@@ -4902,6 +4902,13 @@
|
||||
saveSession();
|
||||
}
|
||||
|
||||
function completeParameterGenerationIfReady() {
|
||||
if (expectedVariants <= 0 || arrivedVariants < expectedVariants) return;
|
||||
if (parameterGenerationState === 'pending' || parameterGenerationState === 'loading') {
|
||||
completeParameterPublication();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTunePopover() {
|
||||
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
||||
if (tuneOpen) { closeTunePopover(); return; }
|
||||
@@ -5796,7 +5803,7 @@
|
||||
setLiveState('CYCLING');
|
||||
showOrUpdateCyclingBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5884,7 +5891,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
||||
} catch (err) {
|
||||
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
||||
@@ -6329,7 +6336,7 @@
|
||||
refreshParamsPanel();
|
||||
positionBar();
|
||||
saveSession();
|
||||
if (parameterGenerationState === 'loading') completeParameterPublication();
|
||||
completeParameterGenerationIfReady();
|
||||
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -6836,6 +6843,7 @@
|
||||
|
||||
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
||||
if (expected > 0) expectedVariants = expected;
|
||||
completeParameterGenerationIfReady();
|
||||
|
||||
if (arrivedVariants > 0) {
|
||||
setLiveState('CYCLING');
|
||||
|
||||
@@ -944,8 +944,42 @@ export async function commitManualEdits({
|
||||
};
|
||||
}
|
||||
|
||||
const repairContext = {
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
};
|
||||
|
||||
const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);
|
||||
const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);
|
||||
const failWithRollback = ({
|
||||
scope = baseRollbackScope,
|
||||
extraFiles = [],
|
||||
failed,
|
||||
files = [],
|
||||
details = {},
|
||||
}) => {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, extraFiles, scope);
|
||||
return {
|
||||
applied: [],
|
||||
failed,
|
||||
files,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
...details,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = repairOnly
|
||||
@@ -965,42 +999,27 @@ export async function commitManualEdits({
|
||||
chatAvailable,
|
||||
});
|
||||
} catch (err) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
failed: batch.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
reason: err.message || String(err),
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
})),
|
||||
files: [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: failed.length > 0
|
||||
? failed
|
||||
: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);
|
||||
@@ -1013,72 +1032,44 @@ export async function commitManualEdits({
|
||||
const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));
|
||||
|
||||
if (conflictingAppliedIds.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),
|
||||
...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
if (unreportedFiles.length > 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: [...rollbackScope, ...unreportedFiles],
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),
|
||||
files: result.files || [],
|
||||
unreportedFiles,
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { unreportedFiles, notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.status === 'done' && reportedAppliedIds.length === 0) {
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
notes: result.notes || [],
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));
|
||||
if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: aiFailed,
|
||||
@@ -1089,21 +1080,10 @@ export async function commitManualEdits({
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedAppliedIds = [];
|
||||
const verificationFailed = [];
|
||||
for (const entry of reportedAppliedEntries) {
|
||||
const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });
|
||||
if (failures.length === 0) {
|
||||
verifiedAppliedIds.push(entry.id);
|
||||
} else {
|
||||
verificationFailed.push({
|
||||
id: entry.id,
|
||||
reason: 'source_verification_failed',
|
||||
failures,
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
const {
|
||||
verifiedIds: verifiedAppliedIds,
|
||||
failed: verificationFailed,
|
||||
} = verifyEntriesAfterRepair({ batch, appliedEntryIds: reportedAppliedIds, files: reportedFiles, cwd });
|
||||
const unreportedEntries = result.status === 'done' || result.status === 'partial'
|
||||
? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))
|
||||
: [];
|
||||
@@ -1133,37 +1113,22 @@ export async function commitManualEdits({
|
||||
reason: 'rolled_back_due_to_failed_entry_source_changed',
|
||||
candidates: candidatesForEntry(batch, entry.id),
|
||||
}));
|
||||
const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);
|
||||
return {
|
||||
applied: [],
|
||||
return failWithRollback({
|
||||
scope: rollbackScope,
|
||||
extraFiles: result.files || [],
|
||||
failed: [
|
||||
...leakedUnapplied,
|
||||
...failed.filter((item) => !leakedIds.has(item.id)),
|
||||
...rolledBackVerified,
|
||||
],
|
||||
files: result.files || [],
|
||||
cleared: 0,
|
||||
count,
|
||||
pageUrl,
|
||||
rolledBackFiles: rollback.rolledBackFiles,
|
||||
rollbackFailures: rollback.rollbackFailures,
|
||||
notes: result.notes || [],
|
||||
...countByPage(cwd),
|
||||
};
|
||||
details: { notes: result.notes || [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (verificationFailed.length > 0) {
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: reportedAppliedIds,
|
||||
files: result.files || [],
|
||||
failed: nonRepairFailed,
|
||||
@@ -1180,16 +1145,7 @@ export async function commitManualEdits({
|
||||
? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))
|
||||
: batch.entries;
|
||||
return repairPostApplyValidation({
|
||||
batch,
|
||||
cwd,
|
||||
pageUrl,
|
||||
count,
|
||||
provider,
|
||||
env,
|
||||
timeoutMs,
|
||||
applyBatchToSource,
|
||||
chatAvailable,
|
||||
transactionId,
|
||||
...repairContext,
|
||||
appliedEntryIds: verifiedAppliedIds.length > 0
|
||||
? verifiedAppliedIds
|
||||
: postCheckEntries.map((entry) => entry.id).filter(Boolean),
|
||||
|
||||
@@ -6,9 +6,16 @@
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { boolFlag, parseArgs, positiveIntFlag, resolveEnum, toCamel } from '../scripts/lib/cli-args.mjs';
|
||||
|
||||
const PROVIDER_SMOKE_SCRIPT = fileURLToPath(new URL('../scripts/smoke-provider-hooks.mjs', import.meta.url));
|
||||
|
||||
describe('parseArgs', () => {
|
||||
it('reads space-separated values', () => {
|
||||
// The regression: without the argv[i+1] lookahead this yielded
|
||||
@@ -125,3 +132,44 @@ describe('resolveEnum', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider hook smoke CLI', () => {
|
||||
it('prints help without requiring a target repository', () => {
|
||||
const result = spawnSync(process.execPath, [PROVIDER_SMOKE_SCRIPT, '--help'], { encoding: 'utf8' });
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /^Usage: bun run smoke:hooks/);
|
||||
assert.match(result.stdout, /target repo must be explicit/);
|
||||
assert.equal(result.stderr, '');
|
||||
});
|
||||
|
||||
it('fails with the same usage guidance when the target repository is omitted', () => {
|
||||
const result = spawnSync(process.execPath, [PROVIDER_SMOKE_SCRIPT], { encoding: 'utf8' });
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.match(result.stderr, /^Usage: bun run smoke:hooks/);
|
||||
assert.match(result.stderr, /target repo must be explicit/);
|
||||
});
|
||||
|
||||
it('preserves the legacy string sentinel for value-less options', () => {
|
||||
const cases = [
|
||||
{ args: ['--repo'], error: /target repo does not exist: .*\/true/ },
|
||||
{ args: ['--repo', '.', '--bundle'], error: /universal bundle does not exist: .*\/true/ },
|
||||
{ args: ['--repo', '.', '--bundle', './missing.zip', '--providers'], error: /universal bundle does not exist: .*\/missing\.zip/ },
|
||||
];
|
||||
|
||||
for (const { args, error } of cases) {
|
||||
const cwd = mkdtempSync(join(tmpdir(), 'impeccable-provider-smoke-cli-'));
|
||||
try {
|
||||
const result = spawnSync(process.execPath, [PROVIDER_SMOKE_SCRIPT, ...args], { cwd, encoding: 'utf8' });
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.doesNotMatch(result.stderr, /TypeError/);
|
||||
assert.match(result.stderr, error);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,8 @@ describe('hook manifest builders', () => {
|
||||
const group = manifest.hooks.PostToolUse[0];
|
||||
const handler = group.hooks[0];
|
||||
|
||||
assert.equal(group.matcher, 'Edit|Write|MultiEdit');
|
||||
assert.equal(group.matcher, 'Edit|Write');
|
||||
assert.doesNotMatch(manifest.description, /MultiEdit/);
|
||||
assert.equal(handler.type, 'command');
|
||||
assert.equal(handler.timeout, 5);
|
||||
assert.equal(handler.statusMessage, 'Checking UI changes');
|
||||
@@ -356,7 +357,7 @@ describe('generated hook artifacts in repo', () => {
|
||||
assert.equal(manifest.description, undefined);
|
||||
|
||||
const handler = manifest.hooks.PostToolUse[0].hooks[0];
|
||||
assert.equal(manifest.hooks.PostToolUse[0].matcher, 'Edit|Write|MultiEdit');
|
||||
assert.equal(manifest.hooks.PostToolUse[0].matcher, 'Edit|Write');
|
||||
expectCommand(handler.command, 'skills/impeccable/scripts/hook.mjs');
|
||||
// Resolves relative to the installed plugin, not a `.claude/skills/` layout.
|
||||
assert.ok(handler.command.includes('${CLAUDE_PLUGIN_ROOT}'),
|
||||
@@ -375,6 +376,15 @@ describe('generated hook artifacts in repo', () => {
|
||||
assert.ok(fs.existsSync(path.join(REPO_ROOT, 'plugin/skills/impeccable/scripts/hook-lib.mjs')));
|
||||
});
|
||||
|
||||
it('keeps the marketplace hook repair matcher aligned with Claude Code', () => {
|
||||
const hookAdmin = fs.readFileSync(
|
||||
path.join(REPO_ROOT, 'plugin/skills/impeccable/scripts/hook-admin.mjs'),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(hookAdmin, /matcher: 'Edit\|Write'/);
|
||||
assert.doesNotMatch(hookAdmin, /matcher: 'Edit\|Write\|MultiEdit'/);
|
||||
});
|
||||
|
||||
it('generated hook runtime can import the bundled detector', async () => {
|
||||
for (const scriptDir of [
|
||||
'.claude/skills/impeccable/scripts',
|
||||
|
||||
@@ -947,6 +947,11 @@ describe('hook-admin.mjs', () => {
|
||||
// impeccable entry must have been stripped, not accumulated.
|
||||
assert.equal(claude.split('skills/impeccable/scripts/hook.mjs').length - 1, 2);
|
||||
assert.match(claude, /"Stop"/);
|
||||
const claudeManifest = JSON.parse(claude);
|
||||
const impeccableGroup = claudeManifest.hooks.PostToolUse.find((group) =>
|
||||
group.hooks?.some((hook) => hook.command?.includes('skills/impeccable/scripts/hook.mjs')));
|
||||
assert.ok(impeccableGroup, 'repaired Claude settings should contain the Impeccable PostToolUse group');
|
||||
assert.equal(impeccableGroup.matcher, 'Edit|Write');
|
||||
|
||||
const codex = fs.readFileSync(path.join(cwd, '.codex', 'hooks.json'), 'utf-8');
|
||||
assert.match(codex, /\.agents\/skills\/impeccable\/scripts\/hook\.mjs/);
|
||||
|
||||
@@ -1032,6 +1032,15 @@ describe('live-browser.js regression guards', () => {
|
||||
/case 'variant_progress':[\s\S]{0,120}?if \(msg\.publicationKind === 'params'\) parameterGenerationState = 'loading';/,
|
||||
'a params-only publication must mark Tune controls loading even though the variant count is unchanged',
|
||||
);
|
||||
assert.match(
|
||||
SOURCE,
|
||||
/function completeParameterGenerationIfReady\(\) \{[\s\S]{0,240}?arrivedVariants < expectedVariants[\s\S]{0,160}?parameterGenerationState === 'pending'[\s\S]{0,120}?completeParameterPublication\(\);/,
|
||||
'the completed variants publication must resolve pending Tune controls even when no params publication follows',
|
||||
);
|
||||
assert.ok(
|
||||
(SOURCE.match(/completeParameterGenerationIfReady\(\);/g) || []).length >= 4,
|
||||
'every DOM, source, and component-preview completion path must resolve pending Tune controls',
|
||||
);
|
||||
assert.match(SOURCE, /revisionDomain: 'browser'/, 'browser checkpoints must use their own revision domain');
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { runUserBot } from './new-work-e2e/user-bot.mjs';
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SERVE = path.join(ROOT, 'skill', 'scripts', 'serve-question.mjs');
|
||||
const GENERATE = path.join(ROOT, 'skill', 'scripts', 'generate-image.mjs');
|
||||
const EMBED_PROMPT = path.join(ROOT, 'skill', 'scripts', 'embed-prompt.mjs');
|
||||
const CATALOG_DIR = path.join(ROOT, 'tests', 'fixtures', 'concept-catalog');
|
||||
|
||||
let playwright;
|
||||
@@ -120,6 +121,10 @@ function spawnSyncGen(prompt, out, size = null) {
|
||||
});
|
||||
}
|
||||
|
||||
function spawnSyncEmbed(args) {
|
||||
return spawnSync(process.execPath, [EMBED_PROMPT, ...args], { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// serve-question interactive cycles
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -1013,6 +1018,57 @@ describe('new-work-e2e: fake image generation', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('reads, scans, and idempotently replaces the prompt embedded in a PNG', () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), 'new-work-img-'));
|
||||
try {
|
||||
const image = makeFakeImage(cwd, 'synthetic IEND source prompt', 'comp.png');
|
||||
const original = readFileSync(image);
|
||||
assert.ok(original.indexOf(Buffer.from('IEND')) < original.lastIndexOf(Buffer.from('IEND')),
|
||||
'the fixture carries IEND bytes in metadata before the real terminator chunk');
|
||||
|
||||
const first = spawnSyncEmbed([image, '--prompt', 'first production prompt']);
|
||||
assert.equal(first.status, 0, first.stderr);
|
||||
assert.equal(spawnSyncEmbed([image, '--read']).stdout.trim(), 'first production prompt');
|
||||
|
||||
const scan = spawnSyncEmbed(['--scan', cwd]);
|
||||
assert.equal(scan.status, 0, scan.stderr);
|
||||
assert.match(scan.stdout, /SCAN: 1 raster, 0 missing/);
|
||||
|
||||
const second = spawnSyncEmbed([image, '--prompt', 'replacement production prompt']);
|
||||
assert.equal(second.status, 0, second.stderr);
|
||||
assert.equal(spawnSyncEmbed([image, '--read']).stdout.trim(), 'replacement production prompt');
|
||||
|
||||
const bytes = readFileSync(image);
|
||||
assert.equal(bytes.toString().match(/impeccable:prompt/g)?.length, 1,
|
||||
're-embedding replaces the existing metadata instead of accumulating chunks');
|
||||
assert.ok(bytes.includes(Buffer.from('SYNTHETIC')),
|
||||
'replacing the prompt preserves unrelated PNG metadata');
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('reads JPEG comments and sidecar fallbacks through the same scan contract', () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), 'new-work-img-'));
|
||||
try {
|
||||
const jpeg = path.join(cwd, 'reference.jpg');
|
||||
const webp = path.join(cwd, 'reference.webp');
|
||||
writeFileSync(jpeg, Buffer.from([0xff, 0xd8, 0xff, 0xda, 0x00, 0x02]));
|
||||
writeFileSync(webp, Buffer.from('RIFF placeholder WEBP'));
|
||||
|
||||
assert.equal(spawnSyncEmbed([jpeg, '--prompt', 'jpeg prompt']).status, 0);
|
||||
assert.equal(spawnSyncEmbed([webp, '--prompt', 'sidecar prompt']).status, 0);
|
||||
assert.equal(spawnSyncEmbed([jpeg, '--read']).stdout.trim(), 'jpeg prompt');
|
||||
assert.equal(spawnSyncEmbed([webp, '--read']).stdout.trim(), 'sidecar prompt');
|
||||
|
||||
const scan = spawnSyncEmbed(['--scan', cwd]);
|
||||
assert.equal(scan.status, 0, scan.stderr);
|
||||
assert.match(scan.stdout, /SCAN: 2 rasters, 0 missing/);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('the SVG variant carries the readable prompt text and SYNTHETIC COMP label', () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), 'new-work-img-'));
|
||||
try {
|
||||
|
||||
@@ -84,11 +84,13 @@ function createFakeUniversalBundle(root, providers = ['.claude', '.agents', '.cu
|
||||
writeFileSync(join(skillDir, 'scripts', 'context.mjs'), 'console.log("local bundle context");\n');
|
||||
}
|
||||
if (providers.includes('.claude')) {
|
||||
mkdirSync(join(bundleRoot, '.claude'), { recursive: true });
|
||||
mkdirSync(join(bundleRoot, '.claude', 'agents'), { recursive: true });
|
||||
writeFileSync(join(bundleRoot, '.claude', 'settings.json'), JSON.stringify({
|
||||
description: 'fresh claude hook',
|
||||
hooks: { PostToolUse: [{ matcher: 'Edit', hooks: [{ type: 'command', command: 'node ".claude/skills/impeccable/scripts/hook.mjs"' }] }] },
|
||||
}, null, 2));
|
||||
writeFileSync(join(bundleRoot, '.claude', 'agents', 'impeccable-finish-reviewer.md'),
|
||||
'---\nname: impeccable-finish-reviewer\ndescription: Reviews a finished build.\n---\nClaude reviewer body.\n');
|
||||
}
|
||||
if (providers.includes('.cursor')) {
|
||||
mkdirSync(join(bundleRoot, '.cursor'), { recursive: true });
|
||||
@@ -228,7 +230,51 @@ describe('copyProviderSkills: symlink handling', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyProviderAgents: Copilot and Cursor subagents', () => {
|
||||
describe('copyProviderAgents: Claude, Copilot, and Cursor subagents', () => {
|
||||
test('Claude project and user scopes use .claude/agents, with project copies taking precedence', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-agents-claude-'));
|
||||
const home = mkdtempSync(join(tmpdir(), 'imp-agents-claude-home-'));
|
||||
const bundle = createFakeUniversalBundle(tmp, ['.claude']);
|
||||
mkdirSync(join(home, '.claude', 'agents'), { recursive: true });
|
||||
writeFileSync(join(home, '.claude', 'agents', 'impeccable-finish-reviewer.md'), 'stale copy\n');
|
||||
|
||||
const projectResults = copyProviderAgents(bundle, tmp, ['.claude'], { scope: 'project', home });
|
||||
const userResults = copyProviderAgents(bundle, home, ['.claude'], { scope: 'user' });
|
||||
|
||||
expect(projectResults).toHaveLength(1);
|
||||
expect(projectResults[0].shadowed).toEqual([]);
|
||||
expect(userResults).toHaveLength(1);
|
||||
expect(readFileSync(join(tmp, '.claude', 'agents', 'impeccable-finish-reviewer.md'), 'utf8'))
|
||||
.toContain('Claude reviewer body.');
|
||||
expect(readFileSync(join(home, '.claude', 'agents', 'impeccable-finish-reviewer.md'), 'utf8'))
|
||||
.toContain('Claude reviewer body.');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('Claude install and update backfill bundled agents beside an unchanged skill', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-agents-claude-install-'));
|
||||
const home = mkdtempSync(join(tmpdir(), 'imp-agents-claude-install-home-'));
|
||||
execSync('git init', { cwd: tmp });
|
||||
const bundleRoot = createFakeUniversalBundle(tmp, ['.claude']);
|
||||
const env = { ...process.env, HOME: home, IMPECCABLE_BUNDLE_PATH: bundleRoot };
|
||||
const agentPath = join(tmp, '.claude', 'agents', 'impeccable-finish-reviewer.md');
|
||||
|
||||
const installOutput = run('skills install -y --no-hooks --providers=claude', { cwd: tmp, env });
|
||||
expect(installOutput).toContain('Installed Claude Code agents into:');
|
||||
expect(existsSync(agentPath)).toBe(true);
|
||||
|
||||
rmSync(agentPath);
|
||||
const updateOutput = run('skills update -y --no-hooks', { cwd: tmp, env });
|
||||
expect(updateOutput).toContain('Updated');
|
||||
expect(updateOutput).toContain('Installed Claude Code agents into:');
|
||||
expect(existsSync(agentPath)).toBe(true);
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}, 15000);
|
||||
|
||||
test('project scope places agents at .github/agents/ and .cursor/agents/', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-agents-project-'));
|
||||
const bundle = createFakeUniversalBundle(tmp, ['.github', '.cursor']);
|
||||
@@ -262,6 +308,46 @@ describe('copyProviderAgents: Copilot and Cursor subagents', () => {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('skills check accepts current Copilot user agents in a home-rooted checkout', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'imp-agents-check-home-'));
|
||||
execSync('git init', { cwd: home });
|
||||
const bundleRoot = createFakeUniversalBundle(home, ['.github']);
|
||||
const env = { ...process.env, HOME: home, IMPECCABLE_BUNDLE_PATH: bundleRoot };
|
||||
|
||||
run('skills install -y --scope=global --no-hooks --providers=github', { cwd: home, env });
|
||||
expect(existsSync(join(home, '.copilot', 'agents', 'impeccable-finish-reviewer.agent.md'))).toBe(true);
|
||||
expect(existsSync(join(home, '.github', 'agents'))).toBe(false);
|
||||
|
||||
const output = run('skills check', { cwd: home, env });
|
||||
expect(output).toContain('Skills are up to date');
|
||||
expect(output).not.toContain('Updates available');
|
||||
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}, 15000);
|
||||
|
||||
test('inferred home-rooted updates refresh stale or missing Copilot user agents', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'imp-agents-update-home-'));
|
||||
execSync('git init', { cwd: home });
|
||||
const bundleRoot = createFakeUniversalBundle(home, ['.github']);
|
||||
const env = { ...process.env, HOME: home, IMPECCABLE_BUNDLE_PATH: bundleRoot };
|
||||
const userAgent = join(home, '.copilot', 'agents', 'impeccable-finish-reviewer.agent.md');
|
||||
const projectAgent = join(home, '.github', 'agents', 'impeccable-finish-reviewer.agent.md');
|
||||
|
||||
run('skills install -y --scope=global --no-hooks --providers=github', { cwd: home, env });
|
||||
writeFileSync(userAgent, 'stale copy\n');
|
||||
|
||||
run('skills update -y --no-hooks', { cwd: home, env });
|
||||
expect(readFileSync(userAgent, 'utf8')).toContain('Copilot reviewer body.');
|
||||
expect(existsSync(projectAgent)).toBe(false);
|
||||
|
||||
rmSync(userAgent);
|
||||
run('skills update -y --no-hooks', { cwd: home, env });
|
||||
expect(readFileSync(userAgent, 'utf8')).toContain('Copilot reviewer body.');
|
||||
expect(existsSync(projectAgent)).toBe(false);
|
||||
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}, 20000);
|
||||
|
||||
test('project scope reports user-level Copilot agents that shadow the installed ones; Cursor never does (project wins there)', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'imp-agents-shadow-'));
|
||||
const home = mkdtempSync(join(tmpdir(), 'imp-agents-shadow-home-'));
|
||||
|
||||
Reference in New Issue
Block a user