Live: polling rework, source locks, preflight scaffolding, Vue previews

Carved out of #371, minus progressive publication. Everything here works
against real project source the way main's Live already does: the agent
writes variants into the file the browser loaded, HMR fires, Accept
promotes and carbonizes. Nothing is staged anywhere.

Poll lanes. Events now carry an explicit priority: accept/discard/exit
ahead of manual_edit_apply/steer/carbonize_cleanup ahead of generate. A
long generate can no longer sit in front of the Accept the user just
clicked. leaseEvent claims its lease before awaiting, so a slow prepare
cannot hand the same event to two pollers.

Source locks. A per-file mutex around every accept and discard path, keyed
on a digest of the absolute path. Staleness is decided by owner-pid
liveness rather than mtime, so a wedged lock clears when its owner dies
instead of after an arbitrary timeout, and a slow-but-live accept is never
stolen from. Only the owning process can release a lock.

Preflight scaffolding. The server runs live-wrap (or live-insert) before
the poll returns and hands the result back as event.scaffold. That walk is
measured at ~7.6s on a large repo; moving it off the agent's critical path
removes a deterministic tool round trip without touching the generated
design. Falls back cleanly to the agent running the helper itself.

Vue previews. previewMode: "vue-component" for Nuxt/Vue targets, matching
the existing Svelte component path: variants compile as real SFCs from a
dev-only directory so the route is never rewritten during generation, and
Vite mounts them without invalidating page state. Accept is the only route
write. Includes a Vue attr tokenizer that normalizes shorthand bindings
(@x, :x, #x) to their canonical forms.

Accept hardening. Every thrown failure now returns mode: 'error' rather
than an ambiguous unhandled result, so a real failure is never classified
as a deliberate manual handoff and silently dropped. The marker search
skips node_modules/.git/dist/build/.impeccable.

Shared CLI arg parsing extracted to scripts/lib/cli-args.mjs.

Assisted-by: Claude Code
This commit is contained in:
Paul Bakaus
2026-07-18 14:11:07 -07:00
parent 97dbaad4a4
commit 3600edc5e9
33 changed files with 62 additions and 2530 deletions
@@ -6,7 +6,6 @@
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'path';
import { fileURLToPath } from 'url';
import {
@@ -18,66 +17,6 @@ import {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURES = path.join(__dirname, 'fixtures', 'antipatterns');
describe('detectText - Astro structural CSS fixtures', () => {
const SHOULD_FLAG = [
'Kinpaku Edge',
'Patina Edge',
'Accent Edge',
'Signal Blue Edge',
'Chromatic Hex Edge',
'Named Red Edge',
'Chromatic Rgb Edge',
'Chromatic Oklch Edge',
// `inset` may follow the offsets/color. Requiring it first missed the same
// stripe written the other legal way.
'Trailing Inset Edge',
'Trailing Inset Token Edge',
'Inset Named Token Edge',
// Only the two offsets are required; blur/spread default to 0.
'Two Length Edge',
'Two Length Trailing Inset Edge',
];
const SHOULD_PASS = [
'Neutral Shadow Token',
'Current Color Edge',
'Selected State Edge',
'Hairline Edge',
'Thick Fill Edge',
'Blurred Edge',
'Narrow Artwork',
// Authored CSS spells neutrals as hex and keywords. isNeutralColor only
// parses the computed function forms and reports everything else as
// chromatic, so routing these through it flagged plain black and gray
// hairlines as the "colored stripe" AI tell.
'Black Hex Edge',
'Black Named Edge',
'Gray Hex Edge',
'Dimgray Named Edge',
'Black Rgb Edge',
'Shorthand Neutral Hex Edge',
// Commented-out CSS is not a live rule.
'Commented Out Edge',
// Trailing `inset` still respects the neutral-color exemption.
'Trailing Inset Neutral Edge',
// The short form still respects the neutral and blur exclusions.
'Two Length Neutral Edge',
'Two Length Blurred Edge',
];
it('Astro style blocks flag unresolved chromatic inset stripes only', () => {
const filePath = path.join(FIXTURES, 'astro-inset-shadow-stripe.astro');
const source = fs.readFileSync(filePath, 'utf8');
const findings = detectText(source, filePath).filter(r => r.antipattern === 'side-tab');
const snippets = findings.map(r => r.snippet || '').join(' | ');
for (const heading of SHOULD_FLAG) {
assert.match(snippets, new RegExp(`data-case=${JSON.stringify(heading)}`), `expected "${heading}" to flag`);
}
for (const heading of SHOULD_PASS) {
assert.doesNotMatch(snippets, new RegExp(`data-case=${JSON.stringify(heading)}`), `"${heading}" should pass`);
}
});
});
describe('detectHtml — static HTML/CSS fixtures', () => {
it('should-flag: catches border anti-patterns', async () => {
const f = await detectHtml(path.join(FIXTURES, 'should-flag.html'));
@@ -1,88 +0,0 @@
---
const title = 'Astro inset shadow stripe regression';
---
<main>
<h1>{title}</h1>
<section aria-labelledby="should-flag">
<h2 id="should-flag">Should flag</h2>
<article data-case="Kinpaku Edge"><h3>Kinpaku Edge</h3></article>
<article data-case="Patina Edge"><h3>Patina Edge</h3></article>
<article data-case="Accent Edge"><h3>Accent Edge</h3></article>
<article data-case="Signal Blue Edge"><h3>Signal Blue Edge</h3></article>
<article data-case="Chromatic Hex Edge"><h3>Chromatic Hex Edge</h3></article>
<article data-case="Named Red Edge"><h3>Named Red Edge</h3></article>
<article data-case="Chromatic Rgb Edge"><h3>Chromatic Rgb Edge</h3></article>
<article data-case="Chromatic Oklch Edge"><h3>Chromatic Oklch Edge</h3></article>
<article data-case="Trailing Inset Edge"><h3>Trailing Inset Edge</h3></article>
<article data-case="Trailing Inset Token Edge"><h3>Trailing Inset Token Edge</h3></article>
<article data-case="Inset Named Token Edge"><h3>Inset Named Token Edge</h3></article>
<article data-case="Two Length Edge"><h3>Two Length Edge</h3></article>
<article data-case="Two Length Trailing Inset Edge"><h3>Two Length Trailing Inset Edge</h3></article>
</section>
<section aria-labelledby="should-pass">
<h2 id="should-pass">Should pass</h2>
<article data-case="Neutral Shadow Token"><h3>Neutral Shadow Token</h3></article>
<article data-case="Current Color Edge"><h3>Current Color Edge</h3></article>
<article data-case="Selected State Edge" aria-current="page"><h3>Selected State Edge</h3></article>
<article data-case="Hairline Edge"><h3>Hairline Edge</h3></article>
<article data-case="Thick Fill Edge"><h3>Thick Fill Edge</h3></article>
<article data-case="Blurred Edge"><h3>Blurred Edge</h3></article>
<article data-case="Narrow Artwork"><h3>Narrow Artwork</h3></article>
<article data-case="Black Hex Edge"><h3>Black Hex Edge</h3></article>
<article data-case="Black Named Edge"><h3>Black Named Edge</h3></article>
<article data-case="Gray Hex Edge"><h3>Gray Hex Edge</h3></article>
<article data-case="Dimgray Named Edge"><h3>Dimgray Named Edge</h3></article>
<article data-case="Black Rgb Edge"><h3>Black Rgb Edge</h3></article>
<article data-case="Shorthand Neutral Hex Edge"><h3>Shorthand Neutral Hex Edge</h3></article>
<article data-case="Commented Out Edge"><h3>Commented Out Edge</h3></article>
<article data-case="Trailing Inset Neutral Edge"><h3>Trailing Inset Neutral Edge</h3></article>
<article data-case="Two Length Neutral Edge"><h3>Two Length Neutral Edge</h3></article>
<article data-case="Two Length Blurred Edge"><h3>Two Length Blurred Edge</h3></article>
</section>
</main>
<style is:inline>
[data-case="Kinpaku Edge"] { box-shadow: inset 3px 0 0 var(--ks-kinpaku-deep); }
[data-case="Patina Edge"] { box-shadow: inset 3px 0 0 var(--ks-patina-deep); }
[data-case="Accent Edge"] { box-shadow: inset -4px 0 0 var(--brand-accent); }
[data-case="Signal Blue Edge"] { box-shadow: inset 0 5px 0 var(--signal-blue); }
[data-case="Neutral Shadow Token"] { box-shadow: inset 3px 0 0 var(--shadow-color); }
[data-case="Current Color Edge"] { box-shadow: inset 3px 0 0 currentColor; }
[data-case="Selected State Edge"][aria-current="page"] { box-shadow: inset 3px 0 0 var(--brand-accent); }
[data-case="Hairline Edge"] { box-shadow: inset 2px 0 0 var(--brand-accent); }
[data-case="Thick Fill Edge"] { box-shadow: inset 14px 0 0 var(--brand-accent); }
[data-case="Blurred Edge"] { box-shadow: inset 3px 0 5px var(--brand-accent); }
[data-case="Narrow Artwork"] { width: 24px; box-shadow: inset 3px 0 0 var(--brand-accent); }
/* Literal colors: authored CSS spells neutrals as hex and keywords, not as
the computed rgb()/oklch() forms a browser emits. */
[data-case="Chromatic Hex Edge"] { box-shadow: inset 4px 0 0 #6366f1; }
[data-case="Named Red Edge"] { box-shadow: inset 4px 0 0 red; }
[data-case="Chromatic Rgb Edge"] { box-shadow: inset 4px 0 0 rgb(99, 102, 241); }
[data-case="Chromatic Oklch Edge"] { box-shadow: inset 4px 0 0 oklch(65% 0.18 250); }
[data-case="Black Hex Edge"] { box-shadow: inset 4px 0 0 #000; }
[data-case="Black Named Edge"] { box-shadow: inset 4px 0 0 black; }
[data-case="Gray Hex Edge"] { box-shadow: inset 4px 0 0 #e5e7eb; }
[data-case="Dimgray Named Edge"] { box-shadow: inset 4px 0 0 dimgray; }
[data-case="Black Rgb Edge"] { box-shadow: inset 4px 0 0 rgb(0, 0, 0); }
[data-case="Shorthand Neutral Hex Edge"] { box-shadow: inset 4px 0 0 #1118; }
/* `inset` is order-independent per spec; these paint the same stripe as above. */
[data-case="Trailing Inset Edge"] { box-shadow: 4px 0 0 #6366f1 inset; }
[data-case="Trailing Inset Token Edge"] { box-shadow: 4px 0 0 var(--brand-accent) inset; }
/* The keyword must only be stripped standalone: this token merely contains it. */
[data-case="Inset Named Token Edge"] { box-shadow: inset 4px 0 0 var(--inset-accent); }
[data-case="Trailing Inset Neutral Edge"] { box-shadow: 4px 0 0 #000 inset; }
/* box-shadow takes <length>{2,4}: blur and spread are optional and default to
0, so these paint the same stripe as the four-length forms above. */
[data-case="Two Length Edge"] { box-shadow: inset 4px 0 var(--brand-accent); }
[data-case="Two Length Trailing Inset Edge"] { box-shadow: 0 5px #6366f1 inset; }
[data-case="Two Length Neutral Edge"] { box-shadow: inset 4px 0 #000; }
[data-case="Two Length Blurred Edge"] { box-shadow: inset 4px 0 5px var(--brand-accent); }
/* Commented-out rules are not live CSS.
[data-case="Commented Out Edge"] { box-shadow: inset 4px 0 0 var(--brand-accent); }
*/
</style>
-128
View File
@@ -53,9 +53,7 @@ import {
extractFindingIgnoreValue,
resolveProjectPlatform,
isNativePlatform,
normalizeIgnoreValueEntries,
} from '../skill/scripts/hook-lib.mjs';
import { normalizeIgnoreValueEntries as normalizeIgnoreValueEntriesCli } from '../cli/lib/impeccable-config.mjs';
import { detectHtml, detectText } from '../cli/engine/detect-antipatterns.mjs';
function mkTmp() {
@@ -566,132 +564,6 @@ describe('hook-admin.mjs', () => {
assert.match(status, /ignoreValues:\s+overused-font=inter/);
});
// detector.ignoreValues honours a `files` scope, which is the narrowest way to
// silence one noisy rule on one file. hook-admin could not write it, so the
// only reachable option was ignore-file, which silences every rule for that
// file forever.
it('ignore-value scopes a wildcard to files via --file', () => {
const out = runAdmin([
'ignore-value', 'design-system-font-size', '*',
'--file', 'src/overlay/widget.js',
'--reason', 'Widget builds its own type scale',
]);
assert.match(out, /scoped to src\/overlay\/widget\.js/);
const shared = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8')).detector;
assert.deepEqual(shared.ignoreValues, [{
rule: 'design-system-font-size',
value: '*',
files: ['src/overlay/widget.js'],
createdAt: shared.ignoreValues[0].createdAt,
reason: 'Widget builds its own type scale',
}]);
});
it('ignore-value accepts --file=, --files= and repeated --file', () => {
runAdmin(['ignore-value', 'side-tab', '*', '--file=a.css']);
runAdmin(['ignore-value', 'side-tab', '*', '--files=b.css']);
runAdmin(['ignore-value', 'low-contrast', '*', '--file', 'c.css', '--file', 'd.css']);
const shared = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8')).detector;
assert.deepEqual(
shared.ignoreValues.map(({ rule, files }) => ({ rule, files })),
[
{ rule: 'side-tab', files: ['a.css'] },
{ rule: 'side-tab', files: ['b.css'] },
{ rule: 'low-contrast', files: ['c.css', 'd.css'] },
],
'each distinct file scope is its own entry; a rule+value-only key overwrote them',
);
});
it('ignore-value refuses a wildcard with no file scope', () => {
assert.throws(
() => runAdmin(['ignore-value', 'design-system-font-size', '*']),
/Wildcard value ignores must be scoped with --file/,
'a bare wildcard is ignore-rule\'s job, not a per-file waiver',
);
assert.equal(fs.existsSync(getConfigPath(cwd)), false, 'a refused ignore must not write config');
});
it('ignore-value --file requires a glob', () => {
assert.throws(
() => runAdmin(['ignore-value', 'side-tab', '*', '--file']),
/--file requires a glob/,
);
});
it('ignore-value rejects an unknown flag instead of folding it into the value', () => {
// `--shard` (a typo for --shared) used to store the value "inter --shard",
// which matches nothing, while reporting a successful suppression.
assert.throws(
() => runAdmin(['ignore-value', 'overused-font', 'Inter', '--shard']),
/Unknown ignore-value flag: --shard/,
);
assert.equal(fs.existsSync(getConfigPath(cwd)), false);
});
// Every write runs the entries through normalizeIgnoreValueEntries. Emitting a
// different key order than the one on disk rewrote all untouched entries.
it('an unrelated edit leaves existing ignoreValues byte-identical', () => {
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
const seeded = {
detector: {
ignoreRules: [],
ignoreFiles: [],
ignoreValues: [
{
rule: 'bounce-easing',
value: 'bounce-ball',
createdAt: '2026-06-15T04:15:03.164Z',
reason: 'Intentional',
},
{
rule: 'design-system-color',
value: '*',
files: ['site/styles/demo.css'],
createdAt: '2026-06-15T23:37:38.170Z',
reason: 'Deliberate off-system demo',
},
],
},
};
fs.writeFileSync(getConfigPath(cwd), JSON.stringify(seeded, null, 2) + '\n');
const before = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8')).detector.ignoreValues;
runAdmin(['ignore-file', 'some/other/**']);
const after = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8')).detector;
assert.deepEqual(after.ignoreFiles, ['some/other/**'], 'the intended change still lands');
assert.equal(
JSON.stringify(after.ignoreValues),
JSON.stringify(before),
'untouched ignoreValues must keep their exact key order, or every config diff churns',
);
});
// hook-lib.mjs (skill, ships into harness dirs) and cli/lib/impeccable-config.mjs
// (CLI + Pages functions) carry independent copies of this normalizer by
// necessity. They write the same file, so a key-order drift between them makes
// the config churn depending on which tool touched it last.
it('both config normalizers emit identical entries', () => {
const input = [
{ rule: 'BOUNCE-EASING', value: 'Bounce-Ball', reason: ' r ', createdAt: '2026-01-01T00:00:00.000Z' },
{ rule: 'design-system-color', value: '*', files: [' a.css ', 'b.css', 'a.css'], createdAt: '2026-02-02T00:00:00.000Z' },
{ rule: 'side-tab', value: '*', file: 'legacy.css' },
{ rule: '', value: 'dropped' },
];
assert.equal(
JSON.stringify(normalizeIgnoreValueEntries(input)),
JSON.stringify(normalizeIgnoreValueEntriesCli(input)),
'skill/scripts/hook-lib.mjs and cli/lib/impeccable-config.mjs must agree, key order included',
);
// And pin the canonical order itself, which is what the config on disk uses.
const full = { rule: 'side-tab', value: '*', files: ['a.css'], createdAt: '2026-01-01T00:00:00.000Z', reason: 'r' };
assert.deepEqual(
Object.keys(normalizeIgnoreValueEntries([full])[0]),
['rule', 'value', 'files', 'createdAt', 'reason'],
);
});
it('a /impeccable hooks edit preserves sibling hook fields (consent, quiet)', () => {
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
// A recorded per-developer consent in the local file...
-10
View File
@@ -74,16 +74,6 @@ describe('live-accept — marker search must ignore Impeccable state', () => {
assert.doesNotMatch(source, /impeccable-variants-start/, 'the wrapper must be gone from real source');
});
it('retires the sessions staged artifacts and leaves other sessions alone', () => {
seed();
const dir = join(tmp, '.impeccable', 'live', 'artifacts');
writeFileSync(join(dir, 'ffff0000-r1.astro'), SOURCE);
runAccept(tmp, ['--id', 'ab12cd34', '--variant', '1']);
assert.equal(existsSync(join(dir, 'ab12cd34-r1.astro')), false, 'own artifacts must not outlive the session');
assert.equal(existsSync(join(dir, 'ab12cd34-r3.astro')), false);
assert.equal(existsSync(join(dir, 'ffff0000-r1.astro')), true, 'another sessions artifacts must survive');
});
it('discards into real source with an artifact decoy present', () => {
seed({ revisions: 1 });
const result = runAccept(tmp, ['--id', 'ab12cd34', '--discard']);
-318
View File
@@ -660,270 +660,7 @@ for (const { name, fixture } of fixtures) {
}
});
if (['vite8-react-plain', 'astro-vite7', 'nextjs-app-router', 'vite8-sveltekit', 'nuxt-vite7'].includes(name) && shouldRunScenario('progressive')) {
it('reveals variant 1 safely while the remaining variants and params are pending', liveE2eTestOptions, async (t) => {
if (manualOnly || process.env.IMPECCABLE_E2E_MANUAL_SCENARIO) {
t.skip('manual scenario filter is active');
return;
}
const traceEvents = [];
const session = await bootFixtureSession({
name,
fixture,
browser,
agent: createFakeAgent(),
wrapTarget: wrapTargetFromPickedElement,
progressive: true,
progressiveDelayMs: 2500,
trace: (eventName, data = {}) => traceEvents.push({ name: eventName, at: Date.now(), ...data }),
log: (m) => t.diagnostic(m),
});
const { page, tmp, consoleErrors, teardown } = session;
let sourceFile = null;
try {
await waitForHandshake(page);
const pickSelector = fixture.runtime.pickSelector || 'h1.hero-title';
const originalCopy = await page.locator(pickSelector).innerText();
await pickElement(page, pickSelector);
await clickGo(page);
const partial = await waitForProgressiveReviewState(page, 3);
assert.equal(partial.arrived, 1, 'exactly variant 1 is present during the progressive interval');
assert.equal(partial.visible, 1, 'variant 1 is the visible review target');
assert.equal(partial.copy, originalCopy, 'variant 1 preserves the picked copy');
assert.notEqual(partial.acceptPointerEvents, 'none', 'Accept is available for the first reviewable variant');
assert.notEqual(partial.discardPointerEvents, 'none', 'Discard can cancel unfinished generation');
assert.equal(partial.hasParams, false, 'variant 1 has no eager parameter manifest');
assert.equal(partial.tuneVisible, true, 'Tune stays visible while parameter generation is outstanding');
assert.equal(partial.tuneDisabled, true, 'pending Tune is non-interactive until controls arrive');
assert.match(partial.tuneTitle || '', /still being prepared/, 'pending Tune explains its loading state');
assert.equal(partial.paramsPanelVisible, false, 'the Tune popover stays closed until parameter delivery');
sourceFile = await locateSessionFile(tmp);
const isComponentPreview = sourceFile.endsWith('manifest.json');
if (isComponentPreview) {
const manifest = JSON.parse(readFileSync(sourceFile, 'utf-8'));
sourceFile = join(tmp, manifest.sourceFile);
const extension = manifest.componentExtension || 'svelte';
assert.equal(existsSync(join(tmp, manifest.componentDir, `v1.${extension}`)), true, 'partial component preview contains variant 1');
assert.equal(existsSync(join(tmp, manifest.componentDir, 'params.json')), false, 'partial component preview defers parameter manifests');
} else {
const partialSource = readFileSync(sourceFile, 'utf-8');
assert.equal(countSourceVariants(partialSource), 1, 'partial source contains one reviewable variant');
assert.doesNotMatch(partialSource, /data-impeccable-params=/, 'partial source defers parameter manifests');
}
// Keyboard Accept must durably fence the worker before its delayed
// second publication, then return the browser to picking without
// waiting for variants the user no longer wants.
const acceptClickedAt = Date.now();
await clickAccept(page, { expectedVariant: 1 });
await waitForBarHidden(page);
await page.waitForFunction(
() => window.__IMPECCABLE_LIVE_STATE__ === 'PICKING',
{ timeout: 2_000 },
);
const automationAcceptToPickingMs = Date.now() - acceptClickedAt;
const browserAcceptToPickingMs = Number(await page.evaluate(() => document.documentElement.dataset.impeccableAcceptToPickingMs));
const acceptToPickingMs = Number.isFinite(browserAcceptToPickingMs) && browserAcceptToPickingMs > 0
? browserAcceptToPickingMs
: automationAcceptToPickingMs;
t.diagnostic(`Accept dispatch → picker ready: ${acceptToPickingMs}ms (${automationAcceptToPickingMs}ms including Playwright actionability)`);
assert.ok(acceptToPickingMs < 500, `Accept should release the picker within 500ms of dispatch; got ${acceptToPickingMs}ms`);
const finalSource = await waitForSourceClean(sourceFile, 20_000);
assert.match(finalSource, new RegExp(escapeRegExp(originalCopy)), 'early accepted source preserves the original copy');
assert.doesNotMatch(finalSource, /data-impeccable-variant=/, 'early accepted source is free of preview scaffolding');
assert.equal(countSourceVariants(finalSource), 0, 'the delayed worker cannot reinsert later variants');
const firstGenerateId = traceEvents.find((event) => event.name === 'agent.event.received' && event.type === 'generate')?.id;
// Give framework HMR one paint to settle the newly committed tree;
// this stays inside the 1.5s next-pick budget and avoids selecting a
// node instance React is replacing in the same frame.
if (name === 'nextjs-app-router' || name === 'vite8-sveltekit' || name === 'nuxt-vite7') await waitForHandshake(page);
await page.waitForTimeout(250);
await page.mouse.move(1, 1);
const nextPickSelector = name === 'nextjs-app-router'
? 'main.page'
: name === 'vite8-sveltekit'
? 'article.feature-card'
: name === 'nuxt-vite7'
? 'main.page'
: '.hero-hook';
await pickElement(page, nextPickSelector, {
resetPickMode: name === 'nextjs-app-router' || name === 'nuxt-vite7',
position: name === 'nuxt-vite7' ? { x: 12, y: 12 } : undefined,
});
const nextGoAt = Date.now();
await clickGo(page);
let nextGenerateTrace = null;
const pickupDeadline = Date.now() + 1_500;
while (Date.now() < pickupDeadline) {
nextGenerateTrace = traceEvents.find((event) => (
event.name === 'agent.event.received'
&& event.type === 'generate'
&& event.id !== firstGenerateId
));
if (nextGenerateTrace) break;
await new Promise((resolve) => setTimeout(resolve, 20));
}
assert.ok(nextGenerateTrace, 'the poll supervisor picks up the next generation while the canceled worker unwinds');
const nextDispatchToPickupMs = nextGenerateTrace.at - nextGenerateTrace.clientSentAt;
assert.ok(
nextDispatchToPickupMs < 1_500,
`next generation pickup should stay below 1.5s from dispatch; got ${nextDispatchToPickupMs}ms`,
);
t.diagnostic(`Next Go dispatch → generation pickup: ${nextDispatchToPickupMs}ms (${nextGenerateTrace.at - nextGoAt}ms including Playwright actionability)`);
if (process.env.IMPECCABLE_E2E_METRICS_FILE) {
appendFileSync(process.env.IMPECCABLE_E2E_METRICS_FILE, JSON.stringify({
acceptToPickingMs,
nextGoToPickupMs: nextDispatchToPickupMs,
automationAcceptToPickingMs,
automationNextGoToPickupMs: nextGenerateTrace.at - nextGoAt,
fixture: name,
at: new Date().toISOString(),
}) + '\n');
}
assert.ok(
traceEvents.some((event) => event.name === 'agent.scaffold.reused'),
'agent reuses the server preflight scaffold',
);
assert.equal(
traceEvents.some((event) => event.name === 'agent.scaffold.start'),
false,
'agent does not repeat deterministic source discovery after preflight',
);
const generateTrace = traceEvents.find((event) => event.name === 'agent.event.received' && event.type === 'generate');
assert.ok(generateTrace?.id, 'generate trace exposes the durable session id');
const generationTimings = await waitForGenerationTimings(tmp, generateTrace.id, { requireAllVariants: false });
assert.ok(generationTimings.generation_ready?.at, 'durable timing records when generation work can start');
assert.ok(generationTimings.first_reviewable?.at, 'durable timing records the first reviewable variant');
assert.equal(generationTimings.all_variants_ready, undefined, 'canceled work never records all variants ready');
const realErrors = consoleErrors.filter((error) =>
!/(Download the React DevTools|StrictMode|Failed to load resource: the server responded with a status of 404)/i.test(error),
);
if (fixture.runtime.probe?.expectConsoleClean) {
assert.deepEqual(realErrors, [], 'progressive HMR and early-action guards produce no browser errors');
} else if (realErrors.length > 0) {
t.diagnostic(`Known framework HMR console noise during progressive source rewrites: ${realErrors.length} error(s)`);
for (const error of realErrors) t.diagnostic(error.split('\n')[0]);
}
} finally {
await teardownAndResetBrowser(teardown);
}
});
}
if (name === 'vite8-react-plain' && shouldRunScenario('progressive')) {
it('accepts variant 2 while variant 3 is still pending', liveE2eTestOptions, async (t) => {
const traceEvents = [];
const session = await bootFixtureSession({
name,
fixture,
browser,
agent: createFakeAgent(),
wrapTarget: wrapTargetFromPickedElement,
progressive: true,
progressiveInitialCount: 2,
progressiveDelayMs: 2500,
trace: (eventName, data = {}) => traceEvents.push({ name: eventName, at: Date.now(), ...data }),
log: (m) => t.diagnostic(m),
});
const { page, tmp, consoleErrors, teardown } = session;
try {
await waitForHandshake(page);
const pickSelector = fixture.runtime.pickSelector || 'h1.hero-title';
const originalCopy = await page.locator(pickSelector).innerText();
await pickElement(page, pickSelector);
await clickGo(page);
const partial = await waitForProgressiveReviewState(page, 3, { arrived: 2, visible: 1 });
assert.equal(partial.arrived, 2, 'variants 1 and 2 arrive before variant 3');
assert.equal(partial.visible, 1, 'variant 1 remains visible until the user advances');
assert.notEqual(partial.acceptPointerEvents, 'none', 'arrived variants remain actionable while the tail is pending');
assert.equal(partial.hasParams, false, 'the partial two-variant revision still defers parameter manifests');
await clickNext(page);
const second = await readProgressiveReviewState(page);
assert.equal(second.visible, 2, 'variant 2 is reviewable before variant 3 exists');
assert.equal(second.copy, originalCopy, 'variant 2 preserves the picked copy');
const wrappedSource = await locateSessionFile(tmp);
const acceptStartedAt = Date.now();
await clickAccept(page, { expectedVariant: 2 });
await waitForBarHidden(page);
await page.waitForFunction(
() => window.__IMPECCABLE_LIVE_STATE__ === 'PICKING',
{ timeout: 2_000 },
);
const browserAcceptMs = Number(await page.evaluate(() => document.documentElement.dataset.impeccableAcceptToPickingMs));
const acceptToPickingMs = Number.isFinite(browserAcceptMs) && browserAcceptMs > 0
? browserAcceptMs
: Date.now() - acceptStartedAt;
assert.ok(acceptToPickingMs < 500, `variant 2 Accept should release the picker within 500ms; got ${acceptToPickingMs}ms`);
const cleanSource = await waitForSourceClean(wrappedSource, 20_000);
assert.match(cleanSource, new RegExp(escapeRegExp(originalCopy)), 'accepted variant 2 preserves source copy');
assert.doesNotMatch(cleanSource, /data-impeccable-variant=/, 'accepted variant 2 leaves no preview scaffolding');
await page.waitForTimeout(2750);
assert.doesNotMatch(readFileSync(wrappedSource, 'utf-8'), /data-impeccable-variant=/, 'the delayed variant 3 write stays fenced');
const generateId = traceEvents.find((event) => event.name === 'agent.event.received' && event.type === 'generate')?.id;
const timings = await waitForGenerationTimings(tmp, generateId, { requireAllVariants: false });
assert.equal(timings.all_variants_ready, undefined, 'accepting variant 2 cancels the unfinished third variant');
const realErrors = consoleErrors.filter((error) =>
!/(Download the React DevTools|StrictMode|Failed to load resource: the server responded with a status of 404)/i.test(error),
);
assert.deepEqual(realErrors, [], 'variant 2 early Accept stays console-clean');
} finally {
await teardownAndResetBrowser(teardown);
}
});
it('promotes pending Tune controls when the params-only revision arrives', liveE2eTestOptions, async (t) => {
const session = await bootFixtureSession({
name,
fixture,
browser,
agent: createFakeAgent(),
wrapTarget: wrapTargetFromPickedElement,
progressive: true,
progressiveDelayMs: 1500,
log: (message) => t.diagnostic(message),
});
const { page, teardown } = session;
try {
await waitForHandshake(page);
await pickElement(page, fixture.runtime.pickSelector || 'h1.hero-title');
await clickGo(page);
const pending = await waitForProgressiveReviewState(page, 3);
assert.equal(pending.tuneVisible, true);
assert.equal(pending.tuneDisabled, true);
await page.waitForFunction(() => {
const root = window.__IMPECCABLE_LIVE_CHROME_CORE__?.root?.()
|| window.__IMPECCABLE_LIVE_UI_ROOT__
|| document;
const tune = root.querySelector('[data-iceq-tune="1"]');
const wrapper = document.querySelector('[data-impeccable-variants]');
return tune?.disabled === false
&& !!wrapper?.querySelector('[data-impeccable-params]');
}, { timeout: 10_000 });
const ready = await readProgressiveReviewState(page);
assert.equal(ready.arrived, 3, 'all variants remain mounted after params publication');
assert.equal(ready.tuneVisible, true);
assert.equal(ready.tuneDisabled, false, 'Tune becomes actionable without another variant arrival');
await clickDiscard(page);
await page.waitForFunction(() => window.__IMPECCABLE_LIVE_STATE__ === 'PICKING', { timeout: 2_000 });
} finally {
await teardownAndResetBrowser(teardown);
}
});
}
if (shouldRunScenario('manual') && Array.isArray(fixture.runtime.manualEditScenarios) && fixture.runtime.manualEditScenarios.length > 0) {
const manualScenarioFilter = process.env.IMPECCABLE_E2E_MANUAL_SCENARIO || '';
@@ -1074,61 +811,6 @@ function recordGenerateEvents(agent, events) {
};
}
async function waitForProgressiveReviewState(page, expected, { arrived: targetArrived = 1, visible: targetVisible = 1 } = {}) {
await installLiveQueryHelpers(page);
await page.waitForFunction(({ variantCount, targetArrived, targetVisible }) => {
const query = window.__impeccableLiveQuery || ((selector) => document.querySelector(selector));
const wrapper = query('[data-impeccable-variants]');
const variants = wrapper?.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
const debugState = window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.();
const arrived = /^(?:svelte|vue)-component$/.test(wrapper?.dataset.impeccablePreview || '')
? Number(debugState?.arrivedVariants || 0)
: variants?.length;
const root = window.__IMPECCABLE_LIVE_CHROME_CORE__?.root?.()
|| window.__IMPECCABLE_LIVE_UI_ROOT__
|| document;
const bar = root.querySelector('#impeccable-live-bar');
return arrived === targetArrived
&& new RegExp(`${targetVisible}\\s*\\/\\s*${variantCount}`).test(bar?.textContent || '')
&& /more arriving/.test(bar?.textContent || '');
}, { variantCount: expected, targetArrived, targetVisible }, { timeout: 15_000 });
return readProgressiveReviewState(page);
}
async function readProgressiveReviewState(page) {
await installLiveQueryHelpers(page);
return page.evaluate(() => {
const query = window.__impeccableLiveQuery || ((selector) => document.querySelector(selector));
const wrapper = query('[data-impeccable-variants]');
const variants = [...(wrapper?.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])') || [])];
const debugState = window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.();
const isSveltePreview = /^(?:svelte|vue)-component$/.test(wrapper?.dataset.impeccablePreview || '');
const visibleVariant = variants.find((variant) => getComputedStyle(variant).display !== 'none');
const root = window.__IMPECCABLE_LIVE_CHROME_CORE__?.root?.()
|| window.__IMPECCABLE_LIVE_UI_ROOT__
|| document;
const buttons = [...root.querySelectorAll('#impeccable-live-bar button')];
const accept = buttons.find((button) => /Accept/.test(button.textContent || ''));
const discard = buttons.find((button) => (button.textContent || '').includes('✕'));
const paramsPanel = root.querySelector('#impeccable-live-params-panel');
const tune = root.querySelector('[data-iceq-tune="1"]');
return {
arrived: isSveltePreview ? Number(debugState?.arrivedVariants || 0) : variants.length,
visible: isSveltePreview ? Number(debugState?.visibleVariant || 0) : Number(visibleVariant?.dataset.impeccableVariant || 0),
copy: isSveltePreview ? (wrapper?.innerText || '') : (visibleVariant?.innerText || ''),
acceptPointerEvents: accept ? getComputedStyle(accept).pointerEvents : null,
discardPointerEvents: discard ? getComputedStyle(discard).pointerEvents : null,
hasParams: variants.some((variant) => variant.hasAttribute('data-impeccable-params')),
tuneVisible: !!tune,
tuneDisabled: tune?.disabled ?? null,
tuneTitle: tune?.title || '',
paramsPanelVisible: !!paramsPanel
&& getComputedStyle(paramsPanel).pointerEvents !== 'none'
&& getComputedStyle(paramsPanel).clipPath === 'inset(0px)',
};
});
}
function countSourceVariants(source) {
return (String(source).match(/<div\s+data-impeccable-variant="(?!original")/g) || []).length;
}
+11 -223
View File
@@ -27,10 +27,6 @@ import { join } from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { completionTypeForAcceptResult } from '../../skill/scripts/live/completion.mjs';
import {
prepareGenerationArtifact,
publishGenerationArtifact,
} from '../../skill/scripts/live/generation-publisher.mjs';
const execFileP = promisify(execFile);
@@ -1394,36 +1390,6 @@ async function writeSvelteComponentVariants({ tmp, wrapInfo, event, output, writ
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
}
async function publishSvelteComponentVariants({ tmp, wrapInfo, event, output, writeParams = true }) {
const prepared = prepareGenerationArtifact({
id: event.id,
sourceFile: wrapInfo.file,
cwd: tmp,
});
if (!prepared.ok) throw new Error(`Svelte publication prepare failed: ${prepared.error}`);
await writeSvelteComponentVariants({
tmp,
wrapInfo: { ...wrapInfo, file: prepared.artifactFile },
event,
output,
writeParams,
});
const published = publishGenerationArtifact({
id: event.id,
epoch: prepared.epoch,
sourceFile: wrapInfo.file,
artifactFile: prepared.artifactFile,
expectedSourceHash: prepared.expectedSourceHash,
arrivedVariants: output.variants.length,
expectedVariants: event.count,
cwd: tmp,
});
if (!published.ok) throw new Error(`Svelte publication failed: ${published.error}`);
return published;
}
async function writeVueComponentVariants({ tmp, wrapInfo, event, output, writeParams = true }) {
const manifestPath = path.join(tmp, wrapInfo.file);
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
@@ -1465,92 +1431,6 @@ async function writeVueComponentVariants({ tmp, wrapInfo, event, output, writePa
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
}
async function publishVueComponentVariants({ tmp, wrapInfo, event, output, writeParams = true }) {
const prepared = prepareGenerationArtifact({ id: event.id, sourceFile: wrapInfo.file, cwd: tmp });
if (!prepared.ok) throw new Error(`Vue publication prepare failed: ${prepared.error}`);
await writeVueComponentVariants({
tmp,
wrapInfo: { ...wrapInfo, file: prepared.artifactFile },
event,
output,
writeParams,
});
const published = publishGenerationArtifact({
id: event.id,
epoch: prepared.epoch,
sourceFile: wrapInfo.file,
artifactFile: prepared.artifactFile,
expectedSourceHash: prepared.expectedSourceHash,
arrivedVariants: output.variants.length,
expectedVariants: event.count,
cwd: tmp,
});
if (!published.ok) throw new Error(`Vue publication failed: ${published.error}`);
return published;
}
async function publishSourceVariants({ tmp, wrapInfo, event, output }) {
const prepared = prepareGenerationArtifact({
id: event.id,
sourceFile: wrapInfo.file,
cwd: tmp,
});
if (!prepared.ok) throw new Error(`Source publication prepare failed: ${prepared.error}`);
await spliceVariantsIntoWrapper({
tmp,
wrapInfo: { ...wrapInfo, file: prepared.artifactFile },
sessionId: event.id,
output,
});
const published = publishGenerationArtifact({
id: event.id,
epoch: prepared.epoch,
sourceFile: wrapInfo.file,
artifactFile: prepared.artifactFile,
expectedSourceHash: prepared.expectedSourceHash,
arrivedVariants: output.variants.length,
expectedVariants: event.count,
cwd: tmp,
});
if (!published.ok) throw new Error(`Source publication failed: ${published.error}`);
return published;
}
async function publishVariantProgress({
base,
token,
event,
wrapInfo,
arrivedVariants,
signal,
revision = 1,
publicationKind = 'variants',
}) {
const previewMode = wrapInfo.previewMode || 'source';
await fetch(`${base}/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token,
type: 'checkpoint',
id: event.id,
revision,
revisionDomain: 'publication',
phase: 'cycling',
reason: 'variants_progress',
arrivedVariants,
expectedVariants: event.count,
sourceFile: wrapInfo.sourceFile || wrapInfo.file,
previewFile: wrapInfo.file,
previewMode,
publicationKind,
}),
signal,
});
}
function variantMarkupHasVisibleContent(markup) {
const text = String(markup || '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
@@ -1683,9 +1563,6 @@ export async function runAgentLoop({
signal,
log = () => {},
trace = () => {},
progressive = false,
progressiveDelayMs = 0,
progressiveInitialCount = 1,
atomicDelayMs = 0,
wrapTarget = { classes: 'hero-title', tag: 'h1' },
steerSourceFile,
@@ -1807,91 +1684,16 @@ export async function runAgentLoop({
// Providers may expose a true split path so variant 1 is written before
// the request for the remaining variants completes.
trace('agent.generate.start', { id: event.id, count: event.count });
const splitProgressive = progressive
&& typeof agent.generateFirstVariant === 'function'
&& typeof agent.generateRemainingVariants === 'function'
&& event.count > 1;
let output;
let firstOutput;
if (splitProgressive) {
firstOutput = normalizeVariantOutput(
await agent.generateFirstVariant(event, { wrapTarget, wrapInfo }),
wrapInfo,
);
firstOutput = {
...firstOutput,
variants: firstOutput.variants.slice(0, 1).map((variant) => ({ ...variant, params: [] })),
};
trace('agent.generate.first_ready', { id: event.id, count: firstOutput.variants.length });
trace('agent.first_variant.write.start', { id: event.id, file: wrapInfo.file });
if (wrapInfo.previewMode === 'svelte-component') {
await publishSvelteComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
} else if (wrapInfo.previewMode === 'vue-component') {
await publishVueComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
} else {
await publishSourceVariants({ tmp, wrapInfo, event, output: firstOutput });
}
await publishVariantProgress({
base,
token,
event,
wrapInfo,
arrivedVariants: firstOutput.variants.length,
signal,
});
trace('agent.first_variant.write.end', { id: event.id, file: wrapInfo.file });
output = normalizeVariantOutput(
await agent.generateRemainingVariants(event, { wrapTarget, wrapInfo, firstOutput }),
wrapInfo,
);
trace('agent.generate.end', { id: event.id, count: output?.variants?.length || 0 });
} else {
output = normalizeVariantOutput(
await agent.generateVariants(event, { wrapTarget, wrapInfo }),
wrapInfo,
);
if (!progressive && atomicDelayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, atomicDelayMs));
}
trace('agent.generate.first_ready', { id: event.id, count: output?.variants?.length || 0 });
if (!progressive || output.variants.length <= 1) {
trace('agent.generate.end', { id: event.id, count: output?.variants?.length || 0 });
}
if (progressive && output.variants.length > 1) {
const initialCount = Math.max(1, Math.min(
Number(progressiveInitialCount) || 1,
output.variants.length - 1,
));
firstOutput = {
...output,
variants: output.variants
.slice(0, initialCount)
.map((variant) => ({ ...variant, params: [] })),
};
trace('agent.first_variant.write.start', { id: event.id, file: wrapInfo.file });
if (wrapInfo.previewMode === 'svelte-component') {
await publishSvelteComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
} else if (wrapInfo.previewMode === 'vue-component') {
await publishVueComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
} else {
await publishSourceVariants({ tmp, wrapInfo, event, output: firstOutput });
}
await publishVariantProgress({
base,
token,
event,
wrapInfo,
arrivedVariants: firstOutput.variants.length,
signal,
});
trace('agent.first_variant.write.end', { id: event.id, file: wrapInfo.file });
if (progressiveDelayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, progressiveDelayMs));
}
trace('agent.generate.end', { id: event.id, count: output?.variants?.length || 0 });
}
let output = normalizeVariantOutput(
await agent.generateVariants(event, { wrapTarget, wrapInfo }),
wrapInfo,
);
if (atomicDelayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, atomicDelayMs));
}
trace('agent.generate.first_ready', { id: event.id, count: output?.variants?.length || 0 });
trace('agent.generate.end', { id: event.id, count: output?.variants?.length || 0 });
if (output.variants.length !== event.count) {
log(`warning: agent returned ${output.variants.length} variants, expected ${event.count}`);
}
@@ -1899,27 +1701,13 @@ export async function runAgentLoop({
// 3. Write the complete set into the deterministic preview target.
trace('agent.write.start', { id: event.id, file: wrapInfo.file });
if (wrapInfo.previewMode === 'svelte-component') {
await publishSvelteComponentVariants({ tmp, wrapInfo, event, output, writeParams: true });
await writeSvelteComponentVariants({ tmp, wrapInfo, event, output, writeParams: true });
} else if (wrapInfo.previewMode === 'vue-component') {
await publishVueComponentVariants({ tmp, wrapInfo, event, output, writeParams: true });
} else if (progressive) {
await publishSourceVariants({ tmp, wrapInfo, event, output });
await writeVueComponentVariants({ tmp, wrapInfo, event, output, writeParams: true });
} else {
await spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId: event.id, output });
}
trace('agent.write.end', { id: event.id, file: wrapInfo.file });
if (progressive) {
await publishVariantProgress({
base,
token,
event,
wrapInfo,
arrivedVariants: output.variants.length,
signal,
revision: 2,
publicationKind: 'params',
});
}
if (process.env.IMPECCABLE_E2E_DEBUG) {
const post = await fs.readFile(path.join(tmp, wrapInfo.file), 'utf-8');
log(`--- post-splice (variants written) ---\n${post}`);
+1 -11
View File
@@ -236,9 +236,6 @@ export async function bootFixtureSession({
prepareTmp,
log = () => {},
trace = () => {},
progressive = false,
progressiveDelayMs = 0,
progressiveInitialCount = 1,
atomicDelayMs = 0,
keepTmp = false,
}) {
@@ -324,18 +321,11 @@ export async function bootFixtureSession({
wrapTarget,
signal: agentAbort.signal,
trace,
progressive,
progressiveDelayMs,
progressiveInitialCount,
atomicDelayMs,
steerSourceFile: runtime.steer?.sourceFile,
steerTarget: runtime.steer?.target,
};
const loops = [runAgentLoop({ ...loopOptions, log: (m) => log('[worker] ' + m) })];
if (progressive) {
loops.push(runAgentLoop({ ...loopOptions, log: (m) => log('[supervisor] ' + m) }));
}
agentDone = Promise.all(loops);
agentDone = Promise.all([runAgentLoop({ ...loopOptions, log: (m) => log('[worker] ' + m) })]);
}
const scheme = runtime.scheme || 'http';
-387
View File
@@ -1,387 +0,0 @@
import assert from 'node:assert/strict';
import { afterEach, beforeEach, describe, it } from 'node:test';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createLiveSessionStore } from '../skill/scripts/live/session-store.mjs';
import {
prepareGenerationArtifact,
publishGenerationArtifact,
sha256,
} from '../skill/scripts/live/generation-publisher.mjs';
describe('transactional generation publisher', () => {
let tmp;
let source;
let artifact;
let store;
beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), 'impeccable-publisher-'));
source = join(tmp, 'page.html');
artifact = join(tmp, 'variant.html');
writeFileSync(source, '<main><div data-impeccable-variants="abc12345"><div data-impeccable-variant="original">Original</div></div></main>');
store = createLiveSessionStore({ cwd: tmp, sessionId: 'abc12345' });
store.appendEvent({
type: 'generate',
id: 'abc12345',
generationEpoch: 1,
action: 'polish',
count: 3,
element: { outerHTML: '<main>Original</main>' },
});
});
afterEach(() => rmSync(tmp, { recursive: true, force: true }));
it('atomically publishes an artifact that matches the fenced source revision', () => {
const before = readFileSync(source, 'utf-8');
writeFileSync(artifact, '<main><div data-impeccable-variants="abc12345"><div data-impeccable-variant="original">Original</div><div data-impeccable-variant="1">Variant</div></div></main>');
const result = publishGenerationArtifact({
id: 'abc12345',
epoch: 1,
sourceFile: source,
artifactFile: artifact,
expectedSourceHash: sha256(before),
expectedVariants: 3,
cwd: tmp,
});
assert.equal(result.ok, true, JSON.stringify(result));
assert.equal(result.arrivedVariants, 1);
assert.equal(readFileSync(source, 'utf-8'), readFileSync(artifact, 'utf-8'));
const snapshot = store.getSnapshot('abc12345');
assert.equal(snapshot.phase, 'variants_progress');
assert.equal(snapshot.publishedRevision, 1);
assert.equal(snapshot.deliveredVariants['1'].digest, result.digest);
});
it('prepares a revision artifact with the current epoch and source fence', () => {
const result = prepareGenerationArtifact({ id: 'abc12345', sourceFile: source, cwd: tmp });
assert.equal(result.ok, true);
assert.equal(result.epoch, 1);
assert.equal(result.revision, 1);
assert.equal(result.expectedSourceHash, sha256(readFileSync(source, 'utf-8')));
assert.equal(readFileSync(join(tmp, result.artifactFile), 'utf-8'), readFileSync(source, 'utf-8'));
});
it('rejects a late publication after early accept without touching source', () => {
const before = readFileSync(source, 'utf-8');
writeFileSync(artifact, '<main><div data-impeccable-variants="abc12345"><div data-impeccable-variant="1">Late</div></div></main>');
store.appendEvent({ type: 'accept', id: 'abc12345', variantId: '1' });
const result = publishGenerationArtifact({
id: 'abc12345',
epoch: 1,
sourceFile: source,
artifactFile: artifact,
expectedSourceHash: sha256(before),
cwd: tmp,
});
assert.deepEqual(result, {
ok: false,
error: 'stale_generation_epoch',
canceled: true,
phase: 'accept_requested',
});
assert.equal(readFileSync(source, 'utf-8'), before);
});
it('rejects a stale artifact when source changed after the worker snapshot', () => {
const before = readFileSync(source, 'utf-8');
writeFileSync(artifact, '<main><div data-impeccable-variants="abc12345"><div data-impeccable-variant="1">Variant</div></div></main>');
writeFileSync(source, before.replace('Original', 'Changed'));
const result = publishGenerationArtifact({
id: 'abc12345',
epoch: 1,
sourceFile: source,
artifactFile: artifact,
expectedSourceHash: sha256(before),
cwd: tmp,
});
assert.equal(result.ok, false);
assert.equal(result.error, 'source_hash_mismatch');
assert.match(readFileSync(source, 'utf-8'), /Changed/);
});
it('keeps an already reviewable source variant immutable across revisions', () => {
const firstSource = '<main><div data-impeccable-variants="abc12345"><div data-impeccable-variant="original">Original</div><div data-impeccable-variant="1"><section><div>First</div></section></div></div></main>';
writeFileSync(artifact, firstSource);
const first = publishGenerationArtifact({
id: 'abc12345',
epoch: 1,
sourceFile: source,
artifactFile: artifact,
expectedSourceHash: sha256(readFileSync(source, 'utf-8')),
arrivedVariants: 1,
expectedVariants: 3,
cwd: tmp,
});
assert.equal(first.ok, true);
const prepared = prepareGenerationArtifact({ id: 'abc12345', sourceFile: source, cwd: tmp });
const changed = firstSource.replace('First', 'Silently changed')
.replace('</div></div></main>', '</div><div data-impeccable-variant="2">Second</div></div></main>');
writeFileSync(join(tmp, prepared.artifactFile), changed);
const result = publishGenerationArtifact({
id: 'abc12345',
epoch: prepared.epoch,
sourceFile: source,
artifactFile: prepared.artifactFile,
expectedSourceHash: prepared.expectedSourceHash,
arrivedVariants: 2,
expectedVariants: 3,
cwd: tmp,
});
assert.equal(result.ok, false);
assert.equal(result.error, 'published_variant_changed');
assert.equal(result.variant, 1);
assert.equal(readFileSync(source, 'utf-8'), firstSource);
});
it('allows the deferred parameter manifest without weakening prior markup immutability', () => {
const firstSource = '<main><div data-impeccable-variants="abc12345"><style data-impeccable-css="abc12345">@scope ([data-impeccable-variant="1"]) { h1 { color: red; } }</style><div data-impeccable-variant="original">Original</div><div data-impeccable-variant="1"><h1>First</h1></div></div></main>';
writeFileSync(artifact, firstSource);
const first = publishGenerationArtifact({
id: 'abc12345', epoch: 1, sourceFile: source, artifactFile: artifact,
expectedSourceHash: sha256(readFileSync(source, 'utf-8')), arrivedVariants: 1, expectedVariants: 3, cwd: tmp,
});
assert.equal(first.ok, true);
const prepared = prepareGenerationArtifact({ id: 'abc12345', sourceFile: source, cwd: tmp });
const withParams = firstSource
.replace('<div data-impeccable-variant="1"', '<div data-impeccable-variant="1" data-impeccable-params=\'[{"id":"scale"}]\'')
.replace('</div></main>', '<div data-impeccable-variant="2">Second</div></div></main>');
writeFileSync(join(tmp, prepared.artifactFile), withParams);
const result = publishGenerationArtifact({
id: 'abc12345', epoch: prepared.epoch, sourceFile: source, artifactFile: prepared.artifactFile,
expectedSourceHash: prepared.expectedSourceHash, arrivedVariants: 2, expectedVariants: 3, cwd: tmp,
});
assert.equal(result.ok, true, JSON.stringify(result));
assert.match(readFileSync(source, 'utf-8'), /data-impeccable-params/);
});
it('rejects later source revisions that restyle an already reviewable variant', () => {
const firstSource = '<main><div data-impeccable-variants="abc12345"><style data-impeccable-css="abc12345">@scope ([data-impeccable-variant="1"]) { :scope > h1 { color: red; } }</style><div data-impeccable-variant="original">Original</div><div data-impeccable-variant="1"><h1>First</h1></div></div></main>';
writeFileSync(artifact, firstSource);
const first = publishGenerationArtifact({
id: 'abc12345', epoch: 1, sourceFile: source, artifactFile: artifact,
expectedSourceHash: sha256(readFileSync(source, 'utf-8')), arrivedVariants: 1, expectedVariants: 3, cwd: tmp,
});
assert.equal(first.ok, true);
const prepared = prepareGenerationArtifact({ id: 'abc12345', sourceFile: source, cwd: tmp });
const changed = firstSource.replace('color: red', 'color: blue');
writeFileSync(join(tmp, prepared.artifactFile), changed);
const result = publishGenerationArtifact({
id: 'abc12345', epoch: prepared.epoch, sourceFile: source, artifactFile: prepared.artifactFile,
expectedSourceHash: prepared.expectedSourceHash, arrivedVariants: 1, expectedVariants: 3, cwd: tmp,
});
assert.equal(result.ok, false);
assert.equal(result.error, 'published_variant_css_changed', JSON.stringify(result));
assert.equal(readFileSync(source, 'utf-8'), firstSource);
});
});
describe('transactional Svelte component publisher', () => {
let tmp;
let source;
let manifestPath;
let componentDir;
let store;
beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), 'impeccable-svelte-publisher-'));
source = join(tmp, 'src', 'routes', '+page.svelte');
componentDir = join(tmp, 'node_modules', '.impeccable-live', 'svelte123');
manifestPath = join(componentDir, 'manifest.json');
mkdirSync(join(tmp, 'src', 'routes'), { recursive: true });
mkdirSync(componentDir, { recursive: true });
writeFileSync(source, '<main><h1>{title}</h1></main>\n');
writeFileSync(manifestPath, JSON.stringify({
id: 'svelte123',
previewMode: 'svelte-component',
sourceFile: 'src/routes/+page.svelte',
sourceStartLine: 1,
sourceEndLine: 1,
count: 3,
propContract: [{ prop: 'title', expr: 'title', placeholder: '{title}' }],
originalMarkup: '<main><h1>{title}</h1></main>',
componentDir: 'node_modules/.impeccable-live/svelte123',
runtimeModule: '/node_modules/.impeccable-live/__runtime.js',
}, null, 2) + '\n');
for (let variant = 1; variant <= 3; variant++) {
writeFileSync(join(componentDir, `v${variant}.svelte`), `<main>Stub ${variant}</main>\n`);
}
store = createLiveSessionStore({ cwd: tmp, sessionId: 'svelte123' });
store.appendEvent({
type: 'generate',
id: 'svelte123',
generationEpoch: 1,
action: 'polish',
count: 3,
element: { outerHTML: '<main><h1>Original</h1></main>' },
});
});
afterEach(() => rmSync(tmp, { recursive: true, force: true }));
it('prepares an isolated component directory fenced against the real route', () => {
const result = prepareGenerationArtifact({ id: 'svelte123', sourceFile: manifestPath, cwd: tmp });
assert.equal(result.ok, true);
assert.equal(result.previewMode, 'svelte-component');
assert.equal(result.sourceFile, 'node_modules/.impeccable-live/svelte123/manifest.json');
assert.equal(result.targetSourceFile, 'src/routes/+page.svelte');
assert.equal(result.expectedSourceHash, sha256(readFileSync(source, 'utf-8')));
const artifactManifest = JSON.parse(readFileSync(join(tmp, result.artifactFile), 'utf-8'));
assert.equal(artifactManifest.componentDir, result.componentDir);
assert.equal(readFileSync(join(tmp, result.componentDir, 'v1.svelte'), 'utf-8'), '<main>Stub 1</main>\n');
writeFileSync(join(tmp, result.componentDir, 'v1.svelte'), '<main>Prepared only</main>\n');
assert.equal(readFileSync(join(componentDir, 'v1.svelte'), 'utf-8'), '<main>Stub 1</main>\n');
});
it('publishes components before committing the arrived manifest and journals preview metadata', () => {
const prepared = prepareGenerationArtifact({ id: 'svelte123', sourceFile: manifestPath, cwd: tmp });
const artifactManifestPath = join(tmp, prepared.artifactFile);
const artifactManifest = JSON.parse(readFileSync(artifactManifestPath, 'utf-8'));
artifactManifest.arrivedVariants = 1;
writeFileSync(artifactManifestPath, JSON.stringify(artifactManifest, null, 2) + '\n');
writeFileSync(join(tmp, prepared.componentDir, 'v1.svelte'), '<main>First live variant</main>\n');
const result = publishGenerationArtifact({
id: 'svelte123',
epoch: prepared.epoch,
sourceFile: manifestPath,
artifactFile: artifactManifestPath,
expectedSourceHash: prepared.expectedSourceHash,
arrivedVariants: 1,
expectedVariants: 3,
cwd: tmp,
});
assert.equal(result.ok, true);
assert.equal(result.previewMode, 'svelte-component');
assert.equal(result.sourceFile, 'src/routes/+page.svelte');
assert.equal(result.previewFile, 'node_modules/.impeccable-live/svelte123/manifest.json');
assert.equal(readFileSync(join(componentDir, 'v1.svelte'), 'utf-8'), '<main>First live variant</main>\n');
assert.equal(readFileSync(source, 'utf-8'), '<main><h1>{title}</h1></main>\n');
const liveManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
assert.equal(liveManifest.arrivedVariants, 1);
assert.equal(liveManifest.componentDir, 'node_modules/.impeccable-live/svelte123');
const snapshot = store.getSnapshot('svelte123');
assert.equal(snapshot.arrivedVariants, 1);
assert.equal(snapshot.previewMode, 'svelte-component');
assert.equal(snapshot.previewFile, 'node_modules/.impeccable-live/svelte123/manifest.json');
});
it('keeps published variants immutable across later revisions', () => {
const first = prepareGenerationArtifact({ id: 'svelte123', sourceFile: manifestPath, cwd: tmp });
publishSveltePrepared(first, { arrived: 1, edits: { 1: '<main>First live variant</main>\n' } });
const second = prepareGenerationArtifact({ id: 'svelte123', sourceFile: manifestPath, cwd: tmp });
const before = readFileSync(join(componentDir, 'v1.svelte'), 'utf-8');
const result = publishSveltePrepared(second, {
arrived: 2,
edits: {
1: '<main>Silently changed first variant</main>\n',
2: '<main>Second live variant</main>\n',
},
});
assert.equal(result.ok, false);
assert.equal(result.error, 'published_variant_changed');
assert.equal(result.variant, 1);
assert.equal(readFileSync(join(componentDir, 'v1.svelte'), 'utf-8'), before);
assert.equal(JSON.parse(readFileSync(manifestPath, 'utf-8')).arrivedVariants, 1);
});
it('publishes later variants and params without rewriting an already reviewable variant', () => {
const first = prepareGenerationArtifact({ id: 'svelte123', sourceFile: manifestPath, cwd: tmp });
publishSveltePrepared(first, { arrived: 1, edits: { 1: '<main>First live variant</main>\n' } });
const second = prepareGenerationArtifact({ id: 'svelte123', sourceFile: manifestPath, cwd: tmp });
writeFileSync(join(tmp, second.componentDir, 'params.json'), '{"2":[{"id":"density"}]}\n');
const result = publishSveltePrepared(second, {
arrived: 3,
edits: {
2: '<main>Second live variant</main>\n',
3: '<main>Third live variant</main>\n',
},
});
assert.equal(result.ok, true);
assert.equal(result.arrivedVariants, 3);
assert.equal(readFileSync(join(componentDir, 'v1.svelte'), 'utf-8'), '<main>First live variant</main>\n');
assert.equal(readFileSync(join(componentDir, 'v2.svelte'), 'utf-8'), '<main>Second live variant</main>\n');
assert.equal(existsSync(join(componentDir, 'params.json')), true);
assert.deepEqual(JSON.parse(readFileSync(join(componentDir, 'params.json'), 'utf-8')), {
2: [{ id: 'density' }],
});
});
it('rejects a prepared Svelte publication after Accept without touching live artifacts', () => {
const prepared = prepareGenerationArtifact({ id: 'svelte123', sourceFile: manifestPath, cwd: tmp });
const beforeManifest = readFileSync(manifestPath, 'utf-8');
const beforeVariant = readFileSync(join(componentDir, 'v1.svelte'), 'utf-8');
store.appendEvent({ type: 'accept', id: 'svelte123', variantId: '1' });
const result = publishSveltePrepared(prepared, {
arrived: 1,
edits: { 1: '<main>Too late</main>\n' },
});
assert.equal(result.ok, false);
assert.equal(result.error, 'stale_generation_epoch');
assert.equal(readFileSync(manifestPath, 'utf-8'), beforeManifest);
assert.equal(readFileSync(join(componentDir, 'v1.svelte'), 'utf-8'), beforeVariant);
});
it('rejects a live component directory masquerading as a staged artifact', () => {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
manifest.arrivedVariants = 1;
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
const result = publishGenerationArtifact({
id: 'svelte123',
epoch: 1,
sourceFile: manifestPath,
artifactFile: manifestPath,
expectedSourceHash: sha256(readFileSync(source, 'utf-8')),
arrivedVariants: 1,
expectedVariants: 3,
cwd: tmp,
});
assert.equal(result.ok, false);
assert.equal(result.error, 'artifact_not_staged');
});
function publishSveltePrepared(prepared, { arrived, edits }) {
const artifactManifestPath = join(tmp, prepared.artifactFile);
const artifactManifest = JSON.parse(readFileSync(artifactManifestPath, 'utf-8'));
artifactManifest.arrivedVariants = arrived;
writeFileSync(artifactManifestPath, JSON.stringify(artifactManifest, null, 2) + '\n');
for (const [variant, content] of Object.entries(edits)) {
writeFileSync(join(tmp, prepared.componentDir, `v${variant}.svelte`), content);
}
return publishGenerationArtifact({
id: 'svelte123',
epoch: prepared.epoch,
sourceFile: manifestPath,
artifactFile: artifactManifestPath,
expectedSourceHash: prepared.expectedSourceHash,
arrivedVariants: arrived,
expectedVariants: 3,
cwd: tmp,
});
}
});
+6 -44
View File
@@ -147,7 +147,7 @@ describe('live reference authoring contract', () => {
);
assert.doesNotMatch(
codexLiveMd,
/<\/?(codex|live-progressive)>/,
/<\/?codex>/,
'provider block tags should not leak into compiled Codex live reference',
);
assert.doesNotMatch(
@@ -157,55 +157,17 @@ describe('live reference authoring contract', () => {
);
});
it('gives progressive delivery to the harnesses that opt in, and only those', () => {
it('routes every helper command through the per-provider scripts path', () => {
const liveMd = readFileSync(join(ROOT, 'skill/reference/live.md'), 'utf-8');
const compileFor = (provider) => compileProviderBlocks(liveMd, PROVIDERS[provider].providerTags);
// Codex delegates to unblock a foreground poll; Claude Code polls in a
// background task. Both can publish variant 1 before the trio is finished.
for (const provider of ['codex', 'agents', 'claude-code']) {
const compiled = compileFor(provider);
assert.match(
compiled,
/Transactional progressive delivery/,
`${provider} should get the progressive publish recipe`,
);
assert.match(
compiled,
/Progressive delivery \(Codex, Claude Code\)/,
`${provider} should get the progressive delivery policy`,
);
}
// Everyone else keeps the atomic single-edit path until their poll loop is
// known not to stall on the extra publish calls.
for (const provider of ['cursor', 'gemini']) {
const compiled = compileFor(provider);
assert.doesNotMatch(
compiled,
/Transactional progressive delivery|Progressive delivery \(Codex, Claude Code\)/,
`${provider} has not opted into progressive delivery`,
);
assert.match(compiled, /\*\*Atomic default:\*\*/, `${provider} should keep the atomic path`);
assert.doesNotMatch(
compiled,
/<\/?live-progressive>/,
`capability block tags should not leak into the compiled ${provider} reference`,
);
}
});
it('routes every live-publish command through the per-provider scripts path', () => {
const liveMd = readFileSync(join(ROOT, 'skill/reference/live.md'), 'utf-8');
// The progressive recipe used to hardcode `.agents/skills/...`, which is only
// correct for the Codex repo-skills bundle. Every other harness would have
// been told to run the publisher from a directory its install never creates.
// A recipe that hardcodes `.agents/skills/...` is only correct for the Codex
// repo-skills bundle. Every other harness would be told to run the helper
// from a directory its install never creates.
assert.doesNotMatch(
liveMd,
/node\s+\.[a-z-]+\/skills\/impeccable\/scripts\//,
'live.md must not hardcode a harness config dir; use {{scripts_path}}',
);
assert.match(liveMd, /node \{\{scripts_path\}\}\/live-publish\.mjs --prepare/);
});
it('keeps live preview CSS guidance capability-mode driven', () => {
-15
View File
@@ -80,21 +80,6 @@ describe('live-session-store', () => {
assert.deepEqual(restarted.getSnapshot('planned-session').variantPlan, plan);
});
it('tracks parameter publication separately from variant arrival', () => {
const store = createLiveSessionStore({ cwd: tmp, sessionId: 'parameter-phase' });
store.appendEvent({ type: 'generate', id: 'parameter-phase', count: 3, generationEpoch: 1 });
store.appendEvent({
type: 'variant_published', id: 'parameter-phase', revision: 1,
generationEpoch: 1, arrivedVariants: 3, publicationKind: 'variants',
});
assert.equal(store.getSnapshot('parameter-phase').paramsPublished, false);
store.appendEvent({
type: 'variant_published', id: 'parameter-phase', revision: 2,
generationEpoch: 1, arrivedVariants: 3, publicationKind: 'params',
});
assert.equal(store.getSnapshot('parameter-phase').paramsPublished, true);
});
it('tombstones generation on early accept and ignores late generation writes', () => {
const store = createLiveSessionStore({ cwd: tmp, sessionId: 'early-accept' });
store.appendEvent({
-75
View File
@@ -5,10 +5,6 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createLiveSessionStore } from '../skill/scripts/live/session-store.mjs';
import {
prepareGenerationArtifact,
publishGenerationArtifact,
} from '../skill/scripts/live/generation-publisher.mjs';
import {
inlineVueComponentAccept,
nuxtViteFsModulePath,
@@ -216,75 +212,4 @@ describe('Nuxt Vue component preview', () => {
assert.equal(existsSync(root), false);
});
it('publishes manifest-last, preserves the route, and rejects late work after Accept', () => {
const result = scaffoldVueComponentSession({
id: 'vue12345',
count: 3,
sourceFile: 'app/pages/index.vue',
sourceStartLine: 3,
sourceEndLine: 3,
originalLines: [' <h1 class="hero-title">Hello {{ user.name }}</h1>'],
cwd: tmp,
});
const store = createLiveSessionStore({ cwd: tmp, sessionId: 'vue12345' });
store.appendEvent({
type: 'generate',
id: 'vue12345',
generationEpoch: 1,
count: 3,
action: 'polish',
element: { outerHTML: '<h1>Hello Paul</h1>' },
});
const routeBefore = readFileSync(source, 'utf-8');
const prepared = prepareGenerationArtifact({ id: 'vue12345', sourceFile: result.manifestFile, cwd: tmp });
assert.equal(prepared.ok, true);
assert.equal(prepared.previewMode, 'vue-component');
const artifactManifest = JSON.parse(readFileSync(join(tmp, prepared.artifactFile), 'utf-8'));
artifactManifest.arrivedVariants = 1;
writeFileSync(join(tmp, prepared.artifactFile), JSON.stringify(artifactManifest, null, 2) + '\n');
writeFileSync(join(tmp, prepared.componentDir, 'v1.vue'), '<template><h1>First</h1></template>\n');
const published = publishGenerationArtifact({
id: 'vue12345',
epoch: prepared.epoch,
sourceFile: result.manifestFile,
artifactFile: prepared.artifactFile,
expectedSourceHash: prepared.expectedSourceHash,
arrivedVariants: 1,
expectedVariants: 3,
cwd: tmp,
});
assert.equal(published.ok, true);
assert.equal(published.previewMode, 'vue-component');
assert.equal(readFileSync(source, 'utf-8'), routeBefore);
assert.equal(JSON.parse(readFileSync(join(tmp, result.manifestFile), 'utf-8')).arrivedVariants, 1);
const late = prepareGenerationArtifact({ id: 'vue12345', sourceFile: result.manifestFile, cwd: tmp });
const lateManifest = JSON.parse(readFileSync(join(tmp, late.artifactFile), 'utf-8'));
lateManifest.arrivedVariants = 2;
writeFileSync(join(tmp, late.artifactFile), JSON.stringify(lateManifest, null, 2) + '\n');
writeFileSync(join(tmp, late.componentDir, 'v2.vue'), '<template><h1>Second</h1></template>\n');
store.appendEvent({ type: 'accept', id: 'vue12345', variantId: '1' });
const rejected = publishGenerationArtifact({
id: 'vue12345',
epoch: late.epoch,
sourceFile: result.manifestFile,
artifactFile: late.artifactFile,
expectedSourceHash: late.expectedSourceHash,
arrivedVariants: 2,
expectedVariants: 3,
cwd: tmp,
});
assert.equal(rejected.ok, false);
assert.equal(rejected.error, 'stale_generation_epoch');
assert.equal(readFileSync(source, 'utf-8'), routeBefore);
assert.equal(JSON.parse(readFileSync(join(tmp, result.manifestFile), 'utf-8')).arrivedVariants, 1,
'the manifest must still advertise only the variant published before Accept');
// A rejected publish must not have touched the session dir at all: v2 still
// holds its untouched scaffold stub rather than the late variant's markup.
const v2AfterReject = readFileSync(join(tmp, result.componentDir, 'v2.vue'), 'utf-8');
assert.doesNotMatch(v2AfterReject, /Second/,
'a rejected late publish must not write variant files into the session dir');
assert.match(v2AfterReject, /Variant 2: add scoped CSS here/, 'v2 must still be the scaffold stub');
});
});