mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 07:06:45 +03:00
Field failure from a real Codex session: accepting a variant into Pitch.svelte appended 23 selectors and removed none, so the source's old .decisions grid rules re-attached through the kept root class and forced the accepted board into a stale three-column layout; some appended base rules also landed after the source's media block, weakening the mobile cascade. Two mechanical fixes: - Preview truth: the scaffolder records the seeded selectors (the source rules that styled the replaced selection, which the isolated preview never applied). On accept, any seeded selector the variant does not re-declare is removed; the selector-loss postcondition treats those removals like compiler prunes. A regression test reproduces the exact Pitch shape end to end. - Cascade order: reconciliation inserts new base rules BEFORE existing top-level media blocks instead of appending after them. Init-latency reductions from the same transcript: - live.mjs inlines the resolved surface brief (removes three surface-brief.mjs round-trips including a --help miss before first poll). - The wrap/scaffold payload carries componentStubMarkup, and live.md instructs editing stubs in place (the session read the manifest + stub back and then deleted/recreated the files). - live.md notes that a busy default port usually means the dev server is already running (the session spawned a duplicate). This work was produced with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com>
356 lines
13 KiB
JavaScript
356 lines
13 KiB
JavaScript
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, symlinkSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
extractMatchingSourceCss,
|
|
removeSelectorsFromSvelteSource,
|
|
findSvelteComponentManifest,
|
|
inlineSvelteComponentAccept,
|
|
mergeCssIntoSvelteSource,
|
|
reindentPreservingStructure,
|
|
scaffoldSvelteComponentSession,
|
|
} from '../skill/scripts/live/svelte-component.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO_NODE_MODULES = join(__dirname, '..', 'node_modules');
|
|
|
|
const ROUTE_SOURCE = `<script>
|
|
let stages = [
|
|
{ label: 'Review', detail: 'Bots comment', active: true },
|
|
{ label: 'Resolve', detail: 'Agent fixes', active: false },
|
|
];
|
|
let footerNote = 'All quiet.';
|
|
</script>
|
|
|
|
<main>
|
|
<ol class="pit-board">
|
|
{#each stages as stage, i}
|
|
<li class="stage">
|
|
<span class="label">{stage.label}</span>
|
|
<p class="detail">{stage.detail}</p>
|
|
</li>
|
|
{/each}
|
|
</ol>
|
|
<p class="footer">{footerNote}</p>
|
|
</main>
|
|
|
|
<style>
|
|
.pit-board {
|
|
border-top: 1px solid #333;
|
|
display: flex;
|
|
}
|
|
.stage { padding: 8px; }
|
|
.footer { color: gray; }
|
|
</style>
|
|
`;
|
|
|
|
function write(root, rel, content) {
|
|
const abs = join(root, rel);
|
|
mkdirSync(dirname(abs), { recursive: true });
|
|
writeFileSync(abs, content);
|
|
}
|
|
|
|
describe('svelte component scaffold + accept pipeline', () => {
|
|
let tmp;
|
|
|
|
beforeEach(() => {
|
|
tmp = realpathSync(mkdtempSync(join(tmpdir(), 'impeccable-svelte-accept-')));
|
|
// The scaffolder resolves the app's svelte compiler; link this repo's.
|
|
mkdirSync(join(tmp, 'node_modules'), { recursive: true });
|
|
try {
|
|
symlinkSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp, 'node_modules', 'svelte'), 'dir');
|
|
} catch {
|
|
cpSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp, 'node_modules', 'svelte'), { recursive: true });
|
|
}
|
|
write(tmp, 'package.json', JSON.stringify({ name: 'app', dependencies: { svelte: '^5' } }));
|
|
write(tmp, 'src/routes/+page.svelte', ROUTE_SOURCE);
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
});
|
|
|
|
function scaffold(id = 'testacc1') {
|
|
// The picked element spans the <ol> block: lines 10-17 (1-indexed).
|
|
const originalLines = ROUTE_SOURCE.split('\n').slice(9, 17);
|
|
assert.match(originalLines[0], /<ol/);
|
|
assert.match(originalLines[originalLines.length - 1], /<\/ol>/);
|
|
return scaffoldSvelteComponentSession({
|
|
id,
|
|
count: 2,
|
|
sourceFile: 'src/routes/+page.svelte',
|
|
sourceStartLine: 10,
|
|
sourceEndLine: 17,
|
|
originalLines,
|
|
cwd: tmp,
|
|
});
|
|
}
|
|
|
|
it('scaffolds a v2 contract with the each collection as one structured prop', () => {
|
|
const session = scaffold();
|
|
assert.equal(session.fallback, undefined);
|
|
assert.equal(session.manifest.contractVersion, 2);
|
|
const collection = session.propContract.find((c) => c.kind === 'collection');
|
|
assert.equal(collection.prop, 'stages');
|
|
assert.equal(collection.item.rootTag, 'li');
|
|
const v1 = readFileSync(join(tmp, session.componentDir, 'v1.svelte'), 'utf-8');
|
|
assert.match(v1, /\{#each stages as stage, i\}/);
|
|
assert.match(v1, /\{stage\.label\}/);
|
|
assert.match(v1, /let \{ stages = \[\] \} = \$props\(\)/);
|
|
// Stub CSS is seeded from the route's matching rules.
|
|
assert.match(v1, /border-top: 1px solid #333/);
|
|
});
|
|
|
|
it('falls back to source-preview for markup with component tags', () => {
|
|
const res = scaffoldSvelteComponentSession({
|
|
id: 'fallb1',
|
|
count: 3,
|
|
sourceFile: 'src/routes/+page.svelte',
|
|
sourceStartLine: 1,
|
|
sourceEndLine: 1,
|
|
originalLines: ['<Card title={x} />'],
|
|
cwd: tmp,
|
|
});
|
|
assert.equal(res.fallback, 'source-preview');
|
|
assert.match(res.reason, /component tag/);
|
|
});
|
|
|
|
it('accept merges CSS instead of appending: superseded rules are replaced, dead branches pruned', () => {
|
|
const session = scaffold('acc2');
|
|
// Agent authors variant 1: arrows instead of divider borders, one param.
|
|
write(tmp, join(session.componentDir, 'v1.svelte'), `<script>
|
|
/** @type {{ stages?: Array<Record<string, unknown>> }} */
|
|
let { stages = [] } = $props();
|
|
</script>
|
|
|
|
<ol class="pit-board">
|
|
{#each stages as stage, i}
|
|
<li class="stage">
|
|
<span class="label">{stage.label}</span>
|
|
<p class="detail">{stage.detail}</p>
|
|
</li>
|
|
{/each}
|
|
</ol>
|
|
|
|
<style>
|
|
.pit-board {
|
|
display: flex;
|
|
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
|
|
}
|
|
.stage { padding: calc(var(--p-depth, 6px) + 2px); }
|
|
:global([data-p-density="airy"]) .stage { margin: 16px; }
|
|
:global([data-p-density="snug"]) .stage { margin: 4px; }
|
|
</style>
|
|
`);
|
|
write(tmp, join(session.componentDir, 'params.json'), JSON.stringify({
|
|
1: [
|
|
{ id: 'depth', kind: 'range', min: 0, max: 20, step: 1, default: 6, label: 'Depth' },
|
|
{ id: 'density', kind: 'steps', default: 'airy', label: 'Density', options: [
|
|
{ value: 'airy', label: 'Airy' }, { value: 'snug', label: 'Snug' },
|
|
] },
|
|
],
|
|
}));
|
|
|
|
const manifest = findSvelteComponentManifest('acc2', tmp);
|
|
const result = inlineSvelteComponentAccept(manifest, 1, { depth: 10, density: 'snug' }, tmp);
|
|
assert.equal(result.handled, true, result.error);
|
|
|
|
const out = readFileSync(join(tmp, 'src/routes/+page.svelte'), 'utf-8');
|
|
// Loop restored with original expressions, one each block only.
|
|
assert.equal(out.split('{#each stages as stage, i}').length - 1, 1);
|
|
// Superseded divider border is GONE (replaced, not shadowed).
|
|
assert.doesNotMatch(out, /border-top: 1px solid #333/);
|
|
assert.match(out, /clip-path/);
|
|
// Exactly one .pit-board rule.
|
|
assert.equal(out.split('.pit-board {').length - 1, 1);
|
|
// Range baked with paren-aware substitution.
|
|
assert.match(out, /padding: calc\(10 \+ 2px\)/);
|
|
// Steps: chosen branch folded into the .stage rule, other branch dropped,
|
|
// no data-p attributes anywhere.
|
|
assert.match(out, /margin: 4px/);
|
|
assert.equal(out.split(/\.stage \{/).length - 1, 1);
|
|
assert.doesNotMatch(out, /margin: 16px/);
|
|
assert.doesNotMatch(out, /data-p-/);
|
|
assert.doesNotMatch(out, /var\(--p-/);
|
|
// The untouched .footer rule survives.
|
|
assert.match(out, /\.footer \{ color: gray; \}/);
|
|
// Self-check reports clean.
|
|
assert.equal(result.verify.clean, true, JSON.stringify(result.verify.findings));
|
|
});
|
|
|
|
it('preserves the variant markup indentation structure', () => {
|
|
const session = scaffold('acc3');
|
|
write(tmp, join(session.componentDir, 'v1.svelte'), `<script>
|
|
let { stages = [] } = $props();
|
|
</script>
|
|
|
|
<ol class="pit-board">
|
|
{#each stages as stage, i}
|
|
<li class="stage">
|
|
<div class="deep">
|
|
<span class="label">{stage.label}</span>
|
|
</div>
|
|
</li>
|
|
{/each}
|
|
</ol>
|
|
|
|
<style>
|
|
.deep { display: block; }
|
|
</style>
|
|
`);
|
|
const manifest = findSvelteComponentManifest('acc3', tmp);
|
|
const result = inlineSvelteComponentAccept(manifest, 1, null, tmp);
|
|
assert.equal(result.handled, true, result.error);
|
|
const out = readFileSync(join(tmp, 'src/routes/+page.svelte'), 'utf-8');
|
|
const lines = out.split('\n');
|
|
const deepIdx = lines.findIndex((l) => l.includes('<div class="deep">'));
|
|
const labelIdx = lines.findIndex((l) => l.includes('span class="label"'));
|
|
const deepIndent = lines[deepIdx].match(/^\s*/)[0].length;
|
|
const labelIndent = lines[labelIdx].match(/^\s*/)[0].length;
|
|
// Nested structure survives: label sits deeper than its parent div.
|
|
assert.equal(labelIndent > deepIndent, true, `expected nesting, got ${deepIndent} vs ${labelIndent}`);
|
|
});
|
|
|
|
it('reindentPreservingStructure keeps relative depth', () => {
|
|
const out = reindentPreservingStructure([' <a>', ' <b>', ' </a>'], ' ');
|
|
assert.deepEqual(out, [' <a>', ' <b>', ' </a>']);
|
|
});
|
|
|
|
it('mergeCssIntoSvelteSource creates a style block when none exists', () => {
|
|
const { text } = mergeCssIntoSvelteSource('<div class="x">hi</div>', '.x { color: red; }');
|
|
assert.match(text, /<style>\n\s+\.x \{ color: red; \}\n<\/style>/);
|
|
});
|
|
|
|
it('extractMatchingSourceCss picks only rules that style the selection', () => {
|
|
const css = extractMatchingSourceCss(ROUTE_SOURCE, '<ol class="pit-board"><li class="stage">x</li></ol>');
|
|
assert.match(css, /\.pit-board/);
|
|
assert.match(css, /\.stage/);
|
|
assert.doesNotMatch(css, /\.footer/);
|
|
});
|
|
});
|
|
|
|
describe('review regressions: preview-truth supersession (the Pitch mangle)', () => {
|
|
const PITCH_SOURCE = `<script>
|
|
let verdicts = [
|
|
{ label: 'True positive', detail: 'Fix it' },
|
|
{ label: 'False positive', detail: 'Dismiss it' },
|
|
];
|
|
</script>
|
|
|
|
<section class="pitch">
|
|
<div class="decisions">
|
|
{#each verdicts as verdict}
|
|
<div class="cell">
|
|
<h3>{verdict.label}</h3>
|
|
<p>{verdict.detail}</p>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
|
|
<style>
|
|
.pitch { padding: 40px; }
|
|
.decisions {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
}
|
|
.decisions > .cell { border: 1px solid #333; }
|
|
@media (max-width: 700px) {
|
|
.decisions { grid-template-columns: 1fr; }
|
|
.pitch { padding: 16px; }
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
it('removes seeded rules the variant did not re-declare and orders new base rules before media blocks', () => {
|
|
const tmp2 = realpathSync(mkdtempSync(join(tmpdir(), 'impeccable-pitch-mangle-')));
|
|
try {
|
|
mkdirSync(join(tmp2, 'node_modules'), { recursive: true });
|
|
try {
|
|
symlinkSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp2, 'node_modules', 'svelte'), 'dir');
|
|
} catch {
|
|
cpSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp2, 'node_modules', 'svelte'), { recursive: true });
|
|
}
|
|
write(tmp2, 'package.json', JSON.stringify({ name: 'app' }));
|
|
write(tmp2, 'src/lib/Pitch.svelte', PITCH_SOURCE);
|
|
|
|
// Picked element: the .decisions block (lines 10-17, 1-indexed).
|
|
const lines = PITCH_SOURCE.split('\n');
|
|
const startLine = lines.findIndex((l) => l.includes('class="decisions"')) + 1;
|
|
const endLine = lines.findIndex((l, i) => i >= startLine && l.trim() === '</div>' && lines[i + 1]?.includes('</section>')) + 1;
|
|
const originalLines = lines.slice(startLine - 1, endLine);
|
|
|
|
const session = scaffoldSvelteComponentSession({
|
|
id: 'pitchm1',
|
|
count: 1,
|
|
sourceFile: 'src/lib/Pitch.svelte',
|
|
sourceStartLine: startLine,
|
|
sourceEndLine: endLine,
|
|
originalLines,
|
|
cwd: tmp2,
|
|
});
|
|
assert.equal(session.fallback, undefined, session.reason);
|
|
// Seeded selectors recorded for accept-time supersession.
|
|
assert.equal(session.manifest.seededSelectors.includes('.decisions'), true);
|
|
|
|
// The agent's variant: a NEW class, no re-declaration of .decisions.
|
|
write(tmp2, join(session.componentDir, 'v1.svelte'), `<script>
|
|
let { verdicts = [] } = $props();
|
|
</script>
|
|
|
|
<div class="disposition-board">
|
|
{#each verdicts as verdict}
|
|
<div class="lane">
|
|
<h3>{verdict.label}</h3>
|
|
<p>{verdict.detail}</p>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<style>
|
|
.disposition-board { display: flex; flex-direction: column; gap: 8px; }
|
|
.disposition-board .lane { border-left: 3px solid #7df; padding: 8px 12px; }
|
|
@media (max-width: 700px) {
|
|
.disposition-board .lane { padding: 6px 8px; }
|
|
}
|
|
</style>
|
|
`);
|
|
const manifest = findSvelteComponentManifest('pitchm1', tmp2);
|
|
const result = inlineSvelteComponentAccept(manifest, 1, null, tmp2);
|
|
assert.equal(result.handled, true, result.error);
|
|
const out = readFileSync(join(tmp2, 'src/lib/Pitch.svelte'), 'utf-8');
|
|
|
|
// The superseded grid rules are GONE: they never applied in the
|
|
// preview the user approved, and the root keeps the old class.
|
|
assert.doesNotMatch(out, /grid-template-columns: repeat\(3, 1fr\)/);
|
|
assert.doesNotMatch(out, /\.decisions > \.cell/);
|
|
assert.equal(result.css.superseded.includes('.decisions'), true);
|
|
// The untouched sibling rule survives.
|
|
assert.match(out, /\.pitch \{ padding: 40px; \}/);
|
|
// Source media block survives for the surviving class...
|
|
assert.match(out, /\.pitch \{ padding: 16px; \}/);
|
|
// ...and no longer carries the superseded selector.
|
|
assert.doesNotMatch(out, /\.decisions \{ grid-template-columns: 1fr; \}/);
|
|
// New base rules sit BEFORE the source's @media block (cascade order).
|
|
const baseIdx = out.indexOf('.disposition-board {');
|
|
const mediaIdx = out.indexOf('@media (max-width: 700px)');
|
|
assert.equal(baseIdx > -1 && mediaIdx > -1 && baseIdx < mediaIdx, true,
|
|
`expected base rules before media, got base@${baseIdx} media@${mediaIdx}`);
|
|
assert.equal(result.verify.clean, true, JSON.stringify(result.verify.findings));
|
|
} finally {
|
|
rmSync(tmp2, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('keeps seeded rules the variant re-declares', () => {
|
|
const { text, removed } = removeSelectorsFromSvelteSource('<div class="a">x</div>\n<style>\n .a { color: red; }\n .b { color: blue; }\n</style>', new Set(['.b']));
|
|
assert.match(text, /\.a \{ color: red; \}/);
|
|
assert.doesNotMatch(text, /color: blue/);
|
|
assert.deepEqual(removed, ['.b']);
|
|
});
|
|
});
|