Live v2: root manifest, mount-ack protocol, AST scaffolder, mechanical accept

A ground-up hardening of live mode, driven by a production session in a
nested-app monorepo that hit six distinct failure classes. Full design
rationale in docs/LIVE-REWRITE-PLAN.md; every Codex-reported failure now
has a mechanical fix and a regression test.

Roots: live/roots.mjs resolves appRoot/repoRoot/contextRoot once at boot
(keyed on dev-server configs, not monorepo brand markers), persists a
manifest, and every live CLI re-anchors onto it at startup, so a helper
run from the wrong directory can no longer fork session state. Context
files are discovered upward to the git root.

Render truth: variant_mounted / variant_mount_failed events give the
journal per-variant mount state; failures reach the agent's poll queue,
raise a persistent error card with Retry (no more localStorage wipe), and
an attach probe names root/dev-server mismatches explicitly. The browser
rehydrates from the server when localStorage is gone.

Svelte: the scaffolder now parses with the app's own svelte 5 compiler.
Control flow survives (an each collection crosses the contract as one
structured prop), keyed each blocks hydrate synthetic keys, and anything
a detached preview cannot support falls back to source-preview instead of
shipping a wrong scaffold. Preview modules live in per-publish revision
directories, defeating stale transform caches.

Accept: CSS is reconciled, not appended. Matching selectors are replaced,
params bake from params.json kinds, the compiler's unused-selector pass
prunes superseded rules (pre-existing dead rules protected), a selector-
loss postcondition refuses any write that would drop hand-written rules,
and live-complete refuses to finish while live plumbing remains in source.

Also: framework registry (live/frameworks/) with a crash-safe injection
journal, session-store snapshot caching with read-only reads, protocol
enum consolidation, steer Send button, honest DESIGN-panel empty states.

Testing: new unit suites (roots, AST scaffolder, accept CSS, accept
pipeline, framework conformance); e2e now fails on preview-tree 404s,
proves computed-style mount for every variant, drives the Tune panel
through baked params, and injects failures (broken mounts, republish,
storage loss). New runtime fixtures: monorepo-nested-vite (repo root !=
app root) and vite8-sveltekit-stateful (each blocks + state). Nightly
full-matrix cron. An independent adversarial review pass preceded this
commit; its blocker and major findings are fixed and regression-tested.

This work was produced with AI assistance (Claude Code).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-27 15:09:40 -07:00
co-authored by Claude Code
parent 839dd10079
commit 17dabf4b7e
72 changed files with 9254 additions and 862 deletions
+242
View File
@@ -674,6 +674,62 @@ export async function clickPrev(page) {
await clickBarButton(page, '←');
}
/**
* Wait until one variant step has fully landed: the bar counter, the bar's own
* notion of the visible variant, and (on component previews) the variant that
* is actually mounted all agree.
*
* Reading the counter alone is not enough. The counter can still show the
* previous step when the next click goes out, and two clicks that arrive
* inside one settle window leave the session on a variant nobody asked for.
*/
export async function waitForVariantSettled(page, expected, count, { timeout = 15_000 } = {}) {
await installLiveQueryHelpers(page);
try {
await page.waitForFunction(
({ expected, count, barSel }) => {
const bar = window.__impeccableLiveQuery(barSel);
if (!bar || !(bar.textContent || '').includes(`${expected}/${count}`)) return false;
const debugState = window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.();
if (!debugState) return true;
if (debugState.visibleVariant !== expected) return false;
if (debugState.hasSvelteComponentSession && debugState.mountedSvelteVariant !== expected) return false;
return true;
},
{ expected, count, barSel: BAR_ID },
{ timeout },
);
} catch (err) {
const debugState = await page
.evaluate(() => window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.() || null)
.catch(() => null);
throw new Error(
`variant ${expected}/${count} never settled; debugState=${JSON.stringify(debugState)} (${err.message})`,
);
}
}
/**
* Step the comparison to `targetVariant`, one settled click at a time.
* Returns the variant now visible.
*/
export async function cycleToVariant(page, targetVariant, count, { settleTimeout = 15_000 } = {}) {
let visible = await getVisibleVariant(page);
let steps = 0;
while (visible !== targetVariant) {
if (steps++ > count + 6) {
throw new Error(`variant ${targetVariant} did not become visible; last visible=${visible}`);
}
const forward = visible == null || visible < targetVariant;
const next = visible == null ? 1 : (forward ? visible + 1 : visible - 1);
if (forward) await clickNext(page);
else await clickPrev(page);
await waitForVariantSettled(page, next, count, { timeout: settleTimeout });
visible = await getVisibleVariant(page);
}
return visible;
}
function barButtonMatch(label) {
if (label instanceof RegExp) return { kind: 'regex', source: label.source, flags: label.flags };
if (label && typeof label === 'object' && label.ariaLabel) return { kind: 'aria', value: label.ariaLabel };
@@ -769,6 +825,192 @@ export async function getVisibleVariant(page) {
}
}
// ---------------------------------------------------------------------------
// Tune popover
//
// buildParamsPanel renders one row per param with no stable ids: a label row
// (label span + readout span) followed by the control (range input, toggle
// track button, or a segmented row of buttons). The label text is the only
// handle a user has too, so that is what these helpers match on.
// ---------------------------------------------------------------------------
const TUNE_BUTTON = '[data-iceq-tune="1"]';
const PARAMS_PANEL_ID = '#impeccable-live-params-panel';
/** Open the Tune popover and wait for its rows to render. */
export async function openTunePanel(page, { timeout = 5_000 } = {}) {
await installLiveQueryHelpers(page);
await page.waitForFunction(
(sel) => {
const tune = window.__impeccableLiveQuery(sel);
return Boolean(tune) && tune.disabled === false;
},
TUNE_BUTTON,
{ timeout },
);
await clickLiveControl(page, TUNE_BUTTON);
await page.waitForFunction(
(sel) => (window.__impeccableLiveQuery(sel)?.querySelectorAll(':scope > div > div').length || 0) > 0,
PARAMS_PANEL_ID,
{ timeout },
);
}
/**
* Drag a `range` knob to `value`. Setting `.value` + dispatching `input` is
* what a real drag produces; the panel's listener reads the input, not the
* event, so this exercises the same code path.
*/
export async function setTuneRange(page, label, value) {
await installLiveQueryHelpers(page);
const applied = await page.evaluate(({ panelSel, label, value }) => {
const panel = window.__impeccableLiveQuery(panelSel);
const row = [...(panel?.querySelectorAll(':scope > div > div') || [])]
.find((candidate) => candidate.querySelector('span')?.textContent?.trim() === label);
const input = row?.querySelector('input[type="range"]');
if (!input) return null;
input.value = String(value);
input.dispatchEvent(new Event('input', { bubbles: true }));
return parseFloat(input.value);
}, { panelSel: PARAMS_PANEL_ID, label, value });
if (applied == null) {
throw new Error(`Tune range "${label}" not found. ${await describeTunePanel(page)}`);
}
return applied;
}
/** Panel contents + variant state, for failures that would otherwise be mute. */
async function describeTunePanel(page) {
const snapshot = await page.evaluate((panelSel) => {
const panel = window.__impeccableLiveQuery(panelSel);
const rows = [...(panel?.querySelectorAll(':scope > div > div') || [])]
.map((row) => (row.querySelector('span')?.textContent || '').trim());
const debugState = window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.() || null;
return {
rows,
barText: debugState?.barText || null,
visibleVariant: debugState?.visibleVariant ?? null,
mountedSvelteVariant: debugState?.mountedSvelteVariant ?? null,
state: debugState?.state || null,
};
}, PARAMS_PANEL_ID).catch((err) => ({ error: err.message }));
return `Panel snapshot: ${JSON.stringify(snapshot)}`;
}
/** Click one option of a `steps` param by its visible option label. */
export async function chooseTuneStep(page, label, optionLabel) {
await installLiveQueryHelpers(page);
const clicked = await page.evaluate(({ panelSel, label, optionLabel }) => {
const panel = window.__impeccableLiveQuery(panelSel);
const row = [...(panel?.querySelectorAll(':scope > div > div') || [])]
.find((candidate) => candidate.querySelector('span')?.textContent?.trim() === label);
if (!row) return 'no-row';
const button = [...row.querySelectorAll('button')]
.find((btn) => (btn.textContent || '').trim() === optionLabel);
if (!button) return 'no-option';
button.click();
return 'ok';
}, { panelSel: PARAMS_PANEL_ID, label, optionLabel });
if (clicked !== 'ok') {
throw new Error(`Tune steps "${label}" option "${optionLabel}" not found (${clicked})`);
}
}
// ---------------------------------------------------------------------------
// Mount-error card (component previews)
// ---------------------------------------------------------------------------
const MOUNT_ERROR_ID = '#impeccable-live-mount-error';
const MOUNT_RETRY = '[data-impeccable-mount-retry="true"]';
/** Wait for the persistent mount-error card and return its text. */
export async function waitForMountErrorCard(page, { variant, timeout = 20_000 } = {}) {
await installLiveQueryHelpers(page);
await page.waitForFunction(
({ sel, variant }) => {
const card = window.__impeccableLiveQuery(sel);
if (!card) return false;
if (variant == null) return true;
return (card.textContent || '').includes(`Variant ${variant} failed to load`);
},
{ sel: MOUNT_ERROR_ID, variant: variant ?? null },
{ timeout },
);
return page.evaluate((sel) => window.__impeccableLiveQuery(sel)?.textContent || '', MOUNT_ERROR_ID);
}
export async function isMountErrorCardVisible(page) {
await installLiveQueryHelpers(page);
return page.evaluate((sel) => Boolean(window.__impeccableLiveQuery(sel)), MOUNT_ERROR_ID);
}
/** Click the card's Retry button (re-imports the manifest's current revision). */
export async function clickMountRetry(page) {
await installLiveQueryHelpers(page);
const clicked = await page.evaluate(({ cardSel, retrySel }) => {
const button = window.__impeccableLiveQuery(cardSel)?.querySelector(retrySel);
if (!button) return false;
button.click();
return true;
}, { cardSel: MOUNT_ERROR_ID, retrySel: MOUNT_RETRY });
if (!clicked) throw new Error('mount-error Retry button not found');
}
export async function waitForMountErrorCardGone(page, { timeout = 20_000 } = {}) {
await installLiveQueryHelpers(page);
await page.waitForFunction(
(sel) => !window.__impeccableLiveQuery(sel),
MOUNT_ERROR_ID,
{ timeout },
);
}
/**
* Wait until the picked element renders with `expected` computed font-weight.
* The fake agent gives every variant a distinct weight, so this is the render
* proof that variant N actually mounted (see FAKE_VARIANT_FONT_WEIGHTS).
*/
export async function waitForComputedFontWeight(page, selector, expected, { timeout = 10_000 } = {}) {
try {
await page.waitForFunction(
({ sel, expected }) => {
const query = window.__impeccableLiveQuery || ((s) => document.querySelector(s));
const el = query(sel) || document.querySelector(sel);
return Boolean(el) && getComputedStyle(el).fontWeight === expected;
},
{ sel: selector, expected: String(expected) },
{ timeout },
);
} catch (err) {
const snapshot = await page.evaluate((sel) => {
const el = document.querySelector(sel);
const debugState = window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.() || null;
const mount = document.querySelector('[data-impeccable-component-mount]');
return {
found: Boolean(el),
fontWeight: el ? getComputedStyle(el).fontWeight : null,
outerHTML: el?.outerHTML?.slice(0, 400) || null,
mountHTML: mount?.outerHTML?.slice(0, 400) || null,
barText: debugState?.barText || null,
state: debugState?.state || null,
visibleVariant: debugState?.visibleVariant ?? null,
mountedSvelteVariant: debugState?.mountedSvelteVariant ?? null,
};
}, selector).catch((snapErr) => ({ error: snapErr.message }));
throw new Error(
`expected computed font-weight ${expected} on ${selector}; snapshot: ${JSON.stringify(snapshot)} (${err.message})`,
);
}
}
export async function readComputedFontWeight(page, selector) {
return page.evaluate((sel) => {
const query = window.__impeccableLiveQuery || ((s) => document.querySelector(s));
const el = query(sel) || document.querySelector(sel);
return el ? getComputedStyle(el).fontWeight : null;
}, selector);
}
/**
* Click Accept — sends accept event with current variantId + paramValues.
* The bar transitions to a "Saving..." spinner, then a green confirmed row.