mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
Rip out the dead isolated-preview mode and the private repo's job
Comparing this branch's live against main's turned up two whole features that never made sense here. -2,466 lines. 1. The isolated source-artifact preview was never switched on. `scaffoldSourceArtifactSession` is only reachable via live-wrap's `--isolated`, and nothing passes it: not the server's preflight, not live.md, nothing. Proved it end-to-end — the default wrap writes markers straight into real source and creates no previews/ session. So the mode was wired through three modules, carried its own accept/discard branches, browser branches, server metadata resolution, preview-mode classifier entry, and test suites, and none of it could run. Worse, live.md documented it as the active path and told the agent "The true source is only the publisher's hash fence and must remain byte-identical until Accept." That is false: the wrapper lands in source at scaffold time and each revision rewrites it. An agent following that sentence believes source is protected when it isn't, and the leftover artifacts are what made accept resolve the wrong file in the first real run. live.md now describes what actually happens, including that markers are visible in source until Accept or Discard. Removed: source-artifact.mjs, --isolated, the preflight's isolated option, the accept/discard branches, four dead browser branches, the server's previews/ resolution, the classifier entry, and their tests. Kept the previews/ gitignore pattern: an ignore line for a directory that cannot exist is free, and a test pins it. 2. Quality judging belongs to the private evals repo, which says so. runner/live/README.md there is explicit: the public repo owns protocol correctness, framework coverage, timing, source commit, recovery, and a rubric-free evidence bundle; the private repo owns the task corpus, baselines, comparative judges, and release-quality decisions — "Do not add quality rubrics, competitor comparisons, or broad fixture corpora to the public Live benchmark." This branch added exactly those: an LLM judge scoring 1-10 on "off-brand, generic-AI" (live-rendered-quality.mjs, judge-live-rendered.mjs), a cross-provider comparison with a BRAND_CONTRACT rubric (live-provider-benchmark .mjs, benchmark-live-providers.mjs), and a brand-fidelity fixture corpus. All removed, with bench:live:providers and their suite entries. Also removed tests/framework-fixtures/README.md's "External quality-eval fixtures" section: it documented a bench:live workflow using --fixture-dir, --agent=codex, --action and --evidence-bundle, none of which benchmark-live.mjs implements, plus an evidenceCapture block nothing reads. Kept: timing benchmarks (the public repo's half of that boundary), progressive publication, the source lock, poll lanes, and Nuxt/Vue component previews. Coverage note: deleting the isolated suites took the only tests for `source_locked` classification with them, so the plain wrapper path — now the only non-component preview — gets equivalent accept and discard coverage. Both new tests fail if mode:'error' is removed. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -30,10 +30,6 @@ import {
|
||||
inlineVueComponentAccept,
|
||||
retireVueComponentSession,
|
||||
} from './live/vue-component.mjs';
|
||||
import {
|
||||
findSourceArtifactManifest,
|
||||
removeSourceArtifactSession,
|
||||
} from './live/source-artifact.mjs';
|
||||
import { removeGenerationArtifacts } from './live/generation-publisher.mjs';
|
||||
|
||||
const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
|
||||
@@ -173,78 +169,15 @@ Output (JSON):
|
||||
}
|
||||
|
||||
// Find the file containing this session's markers
|
||||
const sourceArtifactManifest = findSourceArtifactManifest(id, process.cwd());
|
||||
const found = sourceArtifactManifest ? null : findSessionFile(id, process.cwd());
|
||||
const found = findSessionFile(id, process.cwd());
|
||||
const svelteComponentManifest = found ? null : findSvelteComponentManifest(id, process.cwd());
|
||||
const vueComponentManifest = found || svelteComponentManifest ? null : findVueComponentManifest(id, process.cwd());
|
||||
|
||||
if (!found && !sourceArtifactManifest && !svelteComponentManifest && !vueComponentManifest) {
|
||||
if (!found && !svelteComponentManifest && !vueComponentManifest) {
|
||||
console.log(JSON.stringify({ handled: false, error: 'Session markers not found for id: ' + id }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (sourceArtifactManifest) {
|
||||
if (isDiscard) {
|
||||
// Take the source lock like every other discard path. The journalled
|
||||
// discard already fences publication, so this cannot admit a write to a
|
||||
// discarded session; what it prevents is deleting the preview out from
|
||||
// under a publisher mid-critical-section, which turns its clean
|
||||
// stale_generation_epoch into an ENOENT crash.
|
||||
let result;
|
||||
try {
|
||||
result = withSourceLockSync(
|
||||
sourceArtifactManifest.sourcePath,
|
||||
'discard:' + id,
|
||||
() => {
|
||||
removeSourceArtifactSession(id, process.cwd());
|
||||
return { handled: true };
|
||||
},
|
||||
{ waitMs: ACCEPT_LOCK_WAIT_MS },
|
||||
);
|
||||
} catch (err) {
|
||||
result = operationFailure(err);
|
||||
}
|
||||
emitResult({
|
||||
...result,
|
||||
file: sourceArtifactManifest.sourceFile,
|
||||
sourceFile: sourceArtifactManifest.sourceFile,
|
||||
previewMode: sourceArtifactManifest.previewMode,
|
||||
carbonize: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = withSourceLockSync(
|
||||
sourceArtifactManifest.sourcePath,
|
||||
'accept:' + id,
|
||||
() => acceptSourceArtifact(sourceArtifactManifest, variantNum, paramValues),
|
||||
{ waitMs: ACCEPT_LOCK_WAIT_MS },
|
||||
);
|
||||
} catch (err) {
|
||||
result = operationFailure(err);
|
||||
}
|
||||
if (result.handled !== false) {
|
||||
removeSourceArtifactSession(id, process.cwd());
|
||||
try {
|
||||
scrubManualEditsAgainstOriginalBlock(result.acceptedOriginalText || '', process.cwd(), pageUrl);
|
||||
} catch {}
|
||||
}
|
||||
delete result.acceptedOriginalText;
|
||||
if (result.carbonize) {
|
||||
result.todo = 'REQUIRED before next poll: carbonize cleanup in ' + sourceArtifactManifest.sourceFile + '. See reference/live.md "Required after accept".';
|
||||
}
|
||||
emitResult({
|
||||
handled: result.handled !== false,
|
||||
file: sourceArtifactManifest.sourceFile,
|
||||
sourceFile: sourceArtifactManifest.sourceFile,
|
||||
previewMode: sourceArtifactManifest.previewMode,
|
||||
...result,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (vueComponentManifest) {
|
||||
if (isDiscard) {
|
||||
let result;
|
||||
@@ -649,32 +582,6 @@ function buildAcceptedWrappedSource(id, variantNum, lines, targetFile, paramValu
|
||||
};
|
||||
}
|
||||
|
||||
function acceptSourceArtifact(manifest, variantNum, paramValues) {
|
||||
const source = fs.readFileSync(manifest.sourcePath, 'utf-8');
|
||||
const preview = fs.readFileSync(manifest.previewPath, 'utf-8');
|
||||
const original = String(manifest.originalSource || '');
|
||||
if (!original) return { handled: false, error: 'source_artifact_original_missing' };
|
||||
const first = source.indexOf(original);
|
||||
if (first < 0) return { handled: false, error: 'source_artifact_original_changed' };
|
||||
if (source.indexOf(original, first + original.length) >= 0) {
|
||||
return { handled: false, error: 'source_artifact_original_ambiguous' };
|
||||
}
|
||||
const wrapped = source.slice(0, first) + preview + source.slice(first + original.length);
|
||||
const built = buildAcceptedWrappedSource(
|
||||
manifest.id,
|
||||
variantNum,
|
||||
wrapped.split('\n'),
|
||||
manifest.sourcePath,
|
||||
paramValues,
|
||||
);
|
||||
if (built.handled === false) return built;
|
||||
fs.writeFileSync(manifest.sourcePath, built.content, 'utf-8');
|
||||
return {
|
||||
handled: true,
|
||||
carbonize: built.carbonize,
|
||||
acceptedOriginalText: built.acceptedOriginalText,
|
||||
};
|
||||
}
|
||||
|
||||
function readSourceShadowPreviewMeta(content, id) {
|
||||
const escaped = escapeRegExp(id);
|
||||
|
||||
@@ -4923,10 +4923,6 @@
|
||||
return mode === 'svelte-component' || mode === 'vue-component';
|
||||
}
|
||||
|
||||
function isSourceArtifactPreviewMode(mode) {
|
||||
return mode === 'source-artifact';
|
||||
}
|
||||
|
||||
function parseOriginalMarkupElement(originalMarkup) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString('<div id="impeccable-anchor">' + originalMarkup + '</div>', 'text/html');
|
||||
@@ -5630,9 +5626,7 @@
|
||||
injectSvelteComponentsFromManifest(filePath, sessionId);
|
||||
return;
|
||||
}
|
||||
rememberSessionFileMeta(isSourceArtifactPreviewMode(currentPreviewMode)
|
||||
? { previewFile: filePath, previewMode: currentPreviewMode }
|
||||
: { file: filePath });
|
||||
rememberSessionFileMeta({ file: filePath });
|
||||
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
|
||||
fetch(url)
|
||||
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
|
||||
@@ -6340,8 +6334,6 @@
|
||||
rememberSessionFileMeta(msg);
|
||||
if (isFrameworkComponentPreviewMode(msg.previewMode) && msg.previewFile) {
|
||||
injectSvelteComponentsFromManifest(msg.previewFile, msg.id);
|
||||
} else if (isSourceArtifactPreviewMode(msg.previewMode) && msg.previewFile) {
|
||||
injectVariantsFromSource(msg.previewFile, msg.id);
|
||||
} else if ((msg.previewMode === 'source' || !msg.previewMode) && (msg.previewFile || msg.file)) {
|
||||
// Give normal framework HMR the first chance to reconcile its
|
||||
// own managed tree. Nuxt route-module HMR can skip intermediate
|
||||
@@ -8021,13 +8013,6 @@ void main() {
|
||||
const previewFile = normalizeSessionPath(meta.previewFile);
|
||||
const previewMode = meta.previewMode || (isSvelteComponentManifestPath(previewFile || file) ? 'svelte-component' : null);
|
||||
|
||||
if (isSourceArtifactPreviewMode(previewMode)) {
|
||||
currentPreviewMode = previewMode;
|
||||
currentPreviewFile = previewFile || file || currentPreviewFile;
|
||||
currentSourceFile = sourceFile || currentSourceFile;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFrameworkComponentPreviewMode(previewMode) || isSvelteComponentManifestPath(file)) {
|
||||
currentPreviewMode = isFrameworkComponentPreviewMode(previewMode) ? previewMode : 'svelte-component';
|
||||
currentPreviewFile = previewFile || (isSvelteComponentManifestPath(file) ? file : currentPreviewFile);
|
||||
@@ -8126,7 +8111,7 @@ void main() {
|
||||
saveSession();
|
||||
queueCheckpoint(reason || 'browser_restore_without_wrapper');
|
||||
|
||||
const restoreFile = isFrameworkComponentPreviewMode(currentPreviewMode) || isSourceArtifactPreviewMode(currentPreviewMode)
|
||||
const restoreFile = isFrameworkComponentPreviewMode(currentPreviewMode)
|
||||
? currentPreviewFile
|
||||
: (currentSourceFile || currentPreviewFile);
|
||||
if (restoreFile) {
|
||||
|
||||
@@ -998,16 +998,11 @@ function sessionFileMetadataFromPollReply(file) {
|
||||
if (!file || typeof file !== 'string') return { file };
|
||||
const normalized = file.split(path.sep).join('/');
|
||||
const base = { file: normalized };
|
||||
const sourceArtifactPreview = normalized.includes('.impeccable/live/previews/')
|
||||
&& !normalized.endsWith('/manifest.json');
|
||||
const metadataFile = sourceArtifactPreview
|
||||
? normalized.slice(0, normalized.lastIndexOf('/') + 1) + 'manifest.json'
|
||||
: normalized;
|
||||
const metadataFile = normalized;
|
||||
if (!metadataFile.endsWith('/manifest.json') && metadataFile !== 'manifest.json') return base;
|
||||
if (!metadataFile.includes('node_modules/.impeccable-live/')
|
||||
&& !metadataFile.includes('src/lib/impeccable/')
|
||||
&& !metadataFile.includes('/.impeccable-live/')
|
||||
&& !metadataFile.includes('.impeccable/live/previews/')) return base;
|
||||
&& !metadataFile.includes('/.impeccable-live/')) return base;
|
||||
|
||||
let full;
|
||||
try {
|
||||
@@ -1020,15 +1015,12 @@ function sessionFileMetadataFromPollReply(file) {
|
||||
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
||||
if (!['svelte-component', 'vue-component', 'source-artifact'].includes(manifest?.previewMode)
|
||||
if (!['svelte-component', 'vue-component'].includes(manifest?.previewMode)
|
||||
|| !manifest.sourceFile) return base;
|
||||
const previewFile = manifest.previewMode === 'source-artifact'
|
||||
? String(manifest.previewFile || normalized).split(path.sep).join('/')
|
||||
: normalized;
|
||||
return {
|
||||
file: String(manifest.sourceFile).split(path.sep).join('/'),
|
||||
sourceFile: String(manifest.sourceFile).split(path.sep).join('/'),
|
||||
previewFile,
|
||||
previewFile: normalized,
|
||||
previewMode: manifest.previewMode,
|
||||
};
|
||||
} catch {
|
||||
|
||||
@@ -25,10 +25,6 @@ import {
|
||||
scaffoldVueComponentSession,
|
||||
shouldUseVueComponentInjection,
|
||||
} from './live/vue-component.mjs';
|
||||
import {
|
||||
SOURCE_ARTIFACT_PREVIEW_MODE,
|
||||
scaffoldSourceArtifactSession,
|
||||
} from './live/source-artifact.mjs';
|
||||
|
||||
const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
|
||||
|
||||
@@ -59,8 +55,6 @@ Optional:
|
||||
--page-url URL Current page URL. Required when pending manual edits may
|
||||
affect the picked source block. Pending edits are filtered
|
||||
to this page so an edit on /a doesn't bleed into /b.
|
||||
--isolated Keep ordinary HTML/JSX/Astro source untouched during
|
||||
preview; write the wrapper to an isolated Live artifact.
|
||||
--help Show this help message
|
||||
|
||||
Output (JSON):
|
||||
@@ -79,7 +73,6 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const filePath = argVal(args, '--file');
|
||||
const text = argVal(args, '--text');
|
||||
const pageUrl = argVal(args, '--page-url');
|
||||
const isolated = args.includes('--isolated');
|
||||
|
||||
if (!id) { console.error('Missing --id'); process.exit(1); }
|
||||
if (!elementId && !classes && !query) {
|
||||
@@ -302,7 +295,6 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const useSvelteComponent = shouldUseSvelteComponentInjection(targetFile);
|
||||
const useVueComponent = !useSvelteComponent && shouldUseVueComponentInjection(targetFile);
|
||||
const useFrameworkComponent = useSvelteComponent || useVueComponent;
|
||||
const useSourceArtifact = isolated && !useFrameworkComponent;
|
||||
|
||||
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
|
||||
// JSX requires object-literal style and parses string attrs as HTML (which
|
||||
@@ -321,11 +313,8 @@ The agent should insert variant HTML at insertLine.`);
|
||||
// tuck both marker comments INSIDE it. accept/discard then expands its
|
||||
// replacement range to include the wrapper's `<div>` open / close lines
|
||||
// so the entire scaffold gets removed cleanly.
|
||||
const sourceArtifactAttr = useSourceArtifact
|
||||
? ' data-impeccable-preview="' + SOURCE_ARTIFACT_PREVIEW_MODE + '"'
|
||||
: '';
|
||||
const wrapperLines = isJsx ? [
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + sourceArtifactAttr + ' ' + styleContents + '>',
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + ' ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
@@ -336,7 +325,7 @@ The agent should insert variant HTML at insertLine.`);
|
||||
indent + '</div>',
|
||||
] : [
|
||||
indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + sourceArtifactAttr + ' ' + styleContents + '>',
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + ' ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
originalIndented,
|
||||
@@ -353,7 +342,6 @@ The agent should insert variant HTML at insertLine.`);
|
||||
let insertLine;
|
||||
let svelteSession = null;
|
||||
let vueSession = null;
|
||||
let sourceArtifactSession = null;
|
||||
|
||||
if (useSvelteComponent) {
|
||||
// Svelte/SvelteKit resets component-local state on markup HMR updates.
|
||||
@@ -390,21 +378,6 @@ The agent should insert variant HTML at insertLine.`);
|
||||
outputStartLine = 1;
|
||||
outputEndLine = 1;
|
||||
insertLine = 1;
|
||||
} else if (useSourceArtifact) {
|
||||
sourceArtifactSession = scaffoldSourceArtifactSession({
|
||||
id,
|
||||
count,
|
||||
sourceFile: relTargetFile,
|
||||
sourceStartLine: startLine + 1,
|
||||
sourceEndLine: endLine + 1,
|
||||
originalSource: sourceOriginalLines.join('\n'),
|
||||
previewContent: wrapperLines.join('\n'),
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
outputFile = path.resolve(process.cwd(), sourceArtifactSession.previewFile);
|
||||
outputStartLine = 1;
|
||||
outputEndLine = wrapperLines.length + (originalLines.length - 1);
|
||||
insertLine = 6 + (originalLines.length - 1) + 1;
|
||||
} else {
|
||||
// Replace the original element with the wrapper
|
||||
const newLines = [
|
||||
@@ -430,13 +403,12 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const vueComponentAuthoring = useVueComponent ? buildVueComponentCssAuthoring(count) : null;
|
||||
const componentSession = svelteSession || vueSession;
|
||||
const componentPreviewMode = useSvelteComponent ? 'svelte-component' : useVueComponent ? 'vue-component' : undefined;
|
||||
const previewMode = componentPreviewMode || (useSourceArtifact ? SOURCE_ARTIFACT_PREVIEW_MODE : undefined);
|
||||
const previewMode = componentPreviewMode;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
file: outputRelFile,
|
||||
sourceFile: useFrameworkComponent || useSourceArtifact ? relTargetFile : undefined,
|
||||
sourceFile: useFrameworkComponent ? relTargetFile : undefined,
|
||||
previewMode,
|
||||
previewManifest: sourceArtifactSession?.manifestFile,
|
||||
componentDir: componentSession?.componentDir,
|
||||
propContract: componentSession?.propContract,
|
||||
sourceStartLine: useFrameworkComponent ? startLine + 1 : undefined,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// A preview whose variants live outside the user's source: component modules or
|
||||
// an isolated artifact. These keep the real file untouched until Accept, so a
|
||||
// failed accept leaves nothing in source for the agent to hand-edit and must be
|
||||
// reported as a failure rather than reference/live.md's manual-cleanup handoff.
|
||||
// Previously only `svelte-component` was special-cased here, so the same failure
|
||||
// on a Vue or isolated-artifact preview was acknowledged as a success.
|
||||
// A preview whose variants live in component modules rather than in the user's
|
||||
// source. These leave no markers in the real file, so a failed accept gives the
|
||||
// agent nothing to hand-edit and must be reported as a failure rather than
|
||||
// reference/live.md's manual-cleanup handoff. Previously only `svelte-component`
|
||||
// was special-cased, so the same failure on a Vue preview read as success.
|
||||
const PREVIEW_MODES_WITHOUT_SOURCE_MARKERS = new Set([
|
||||
'svelte-component',
|
||||
'vue-component',
|
||||
'source-artifact',
|
||||
]);
|
||||
|
||||
export function completionTypeForAcceptResult(eventType, acceptResult) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { promisify } from 'node:util';
|
||||
const execFileAsync = promisify(execFile);
|
||||
const PREFLIGHT_TIMEOUT_MS = 15_000;
|
||||
|
||||
export function buildGenerationPreflight(event, scriptsDir, { isolated = false } = {}) {
|
||||
export function buildGenerationPreflight(event, scriptsDir) {
|
||||
if (!event || event.type !== 'generate' || !event.id) return null;
|
||||
|
||||
const isInsert = event.mode === 'insert';
|
||||
@@ -14,7 +14,6 @@ export function buildGenerationPreflight(event, scriptsDir, { isolated = false }
|
||||
|
||||
const script = path.join(scriptsDir, isInsert ? 'live-insert.mjs' : 'live-wrap.mjs');
|
||||
const args = [script, '--id', event.id, '--count', String(event.count || 3)];
|
||||
if (!isInsert && isolated) args.push('--isolated');
|
||||
if (isInsert) args.push('--position', target.position);
|
||||
if (target.elementId) args.push('--element-id', target.elementId);
|
||||
if (target.classes) args.push('--classes', target.classes);
|
||||
@@ -39,9 +38,8 @@ export async function runGenerationPreflight(event, {
|
||||
scriptsDir,
|
||||
execFileImpl = execFileAsync,
|
||||
timeoutMs = PREFLIGHT_TIMEOUT_MS,
|
||||
isolated = false,
|
||||
} = {}) {
|
||||
const command = buildGenerationPreflight(event, scriptsDir, { isolated });
|
||||
const command = buildGenerationPreflight(event, scriptsDir);
|
||||
if (!command) {
|
||||
return { ok: false, skipped: true, reason: 'insufficient_locator' };
|
||||
}
|
||||
|
||||
@@ -4,10 +4,6 @@ import { createHash } from 'node:crypto';
|
||||
import { createLiveSessionStore } from './session-store.mjs';
|
||||
import { withSourceLockSync } from './source-lock.mjs';
|
||||
import { getLiveDir, safeSessionId } from '../lib/impeccable-paths.mjs';
|
||||
import {
|
||||
SOURCE_ARTIFACT_PREVIEW_MODE,
|
||||
findSourceArtifactManifest,
|
||||
} from './source-artifact.mjs';
|
||||
|
||||
export function sha256(value) {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
@@ -43,9 +39,7 @@ export function prepareGenerationArtifact({ id, sourceFile, cwd = process.cwd()
|
||||
|
||||
const componentTarget = readComponentPublicationTarget(requestedPath, cwd, id);
|
||||
if (componentTarget?.error) return componentTarget;
|
||||
const sourceArtifactTarget = componentTarget ? null : readSourceArtifactPublicationTarget(requestedPath, cwd, id);
|
||||
if (sourceArtifactTarget?.error) return sourceArtifactTarget;
|
||||
const sourcePath = componentTarget?.sourcePath || sourceArtifactTarget?.sourcePath || requestedPath;
|
||||
const sourcePath = componentTarget?.sourcePath || requestedPath;
|
||||
|
||||
try {
|
||||
return withSourceLockSync(sourcePath, 'generation-prepare:' + id, () => {
|
||||
@@ -56,9 +50,7 @@ export function prepareGenerationArtifact({ id, sourceFile, cwd = process.cwd()
|
||||
return failure('stale_generation_epoch', { canceled: true, phase: snapshot.phase });
|
||||
}
|
||||
const source = fs.readFileSync(sourcePath, 'utf-8');
|
||||
const artifactBase = sourceArtifactTarget
|
||||
? fs.readFileSync(sourceArtifactTarget.previewPath, 'utf-8')
|
||||
: source;
|
||||
const artifactBase = source;
|
||||
const revision = Number(snapshot.publishedRevision || 0) + 1;
|
||||
const artifactDir = path.join(getLiveDir(cwd), 'artifacts');
|
||||
if (componentTarget) {
|
||||
@@ -84,10 +76,6 @@ export function prepareGenerationArtifact({ id, sourceFile, cwd = process.cwd()
|
||||
epoch: Number(snapshot.generationEpoch || 1),
|
||||
revision,
|
||||
sourceFile: relative(cwd, sourcePath),
|
||||
...(sourceArtifactTarget ? {
|
||||
previewFile: relative(cwd, sourceArtifactTarget.previewPath),
|
||||
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
|
||||
} : {}),
|
||||
artifactFile: relative(cwd, artifactPath),
|
||||
expectedSourceHash: sha256(source),
|
||||
};
|
||||
@@ -124,8 +112,6 @@ export function publishGenerationArtifact({
|
||||
|
||||
const componentTarget = readComponentPublicationTarget(requestedPath, cwd, id);
|
||||
if (componentTarget?.error) return componentTarget;
|
||||
const sourceArtifactTarget = componentTarget ? null : readSourceArtifactPublicationTarget(requestedPath, cwd, id);
|
||||
if (sourceArtifactTarget?.error) return sourceArtifactTarget;
|
||||
const artifactManifest = readJson(artifactPath);
|
||||
const isComponentArtifact = isComponentPreviewMode(artifactManifest?.previewMode);
|
||||
if (Boolean(componentTarget) !== isComponentArtifact) {
|
||||
@@ -134,7 +120,7 @@ export function publishGenerationArtifact({
|
||||
if (componentTarget && componentTarget.manifest.previewMode !== artifactManifest?.previewMode) {
|
||||
return failure('artifact_preview_mode_mismatch');
|
||||
}
|
||||
const sourcePath = componentTarget?.sourcePath || sourceArtifactTarget?.sourcePath || requestedPath;
|
||||
const sourcePath = componentTarget?.sourcePath || requestedPath;
|
||||
|
||||
try {
|
||||
return withSourceLockSync(sourcePath, 'generation:' + id + ':' + epoch, () => {
|
||||
@@ -167,9 +153,7 @@ export function publishGenerationArtifact({
|
||||
});
|
||||
}
|
||||
|
||||
const stablePreview = sourceArtifactTarget
|
||||
? fs.readFileSync(sourceArtifactTarget.previewPath, 'utf-8')
|
||||
: current;
|
||||
const stablePreview = current;
|
||||
const artifact = fs.readFileSync(artifactPath, 'utf-8');
|
||||
if (!artifact.includes('data-impeccable-variants="' + id + '"')) {
|
||||
return failure('artifact_missing_session_wrapper');
|
||||
@@ -200,7 +184,7 @@ export function publishGenerationArtifact({
|
||||
const commitStale = staleGenerationFailure(commitSnapshot, epoch);
|
||||
if (commitStale) return commitStale;
|
||||
const artifactHash = sha256(artifact);
|
||||
const publishPath = sourceArtifactTarget?.previewPath || sourcePath;
|
||||
const publishPath = sourcePath;
|
||||
atomicReplace(publishPath, artifact);
|
||||
const revision = Number(commitSnapshot.publishedRevision || 0) + 1;
|
||||
store.appendEvent({
|
||||
@@ -210,10 +194,6 @@ export function publishGenerationArtifact({
|
||||
revision,
|
||||
digest: artifactHash,
|
||||
sourceFile: relative(cwd, sourcePath),
|
||||
...(sourceArtifactTarget ? {
|
||||
previewFile: relative(cwd, publishPath),
|
||||
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
|
||||
} : {}),
|
||||
arrivedVariants: delivered,
|
||||
expectedVariants: Number(expectedVariants || snapshot.expectedVariants || delivered),
|
||||
publicationKind: publicationKind || 'variants',
|
||||
@@ -226,10 +206,6 @@ export function publishGenerationArtifact({
|
||||
revision,
|
||||
digest: artifactHash,
|
||||
sourceFile: relative(cwd, sourcePath),
|
||||
...(sourceArtifactTarget ? {
|
||||
previewFile: relative(cwd, publishPath),
|
||||
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
|
||||
} : {}),
|
||||
arrivedVariants: delivered,
|
||||
expectedVariants: Number(expectedVariants || snapshot.expectedVariants || delivered),
|
||||
publicationKind: publicationKind || 'variants',
|
||||
@@ -366,7 +342,7 @@ function publishComponentArtifact({
|
||||
}
|
||||
|
||||
// Check the fence before writing anything. The prepare→publish gap is exactly
|
||||
// where an Accept lands, and the source-artifact path above rechecks before
|
||||
// where an Accept lands, and the non-component path above rechecks before
|
||||
// its only write. Without the same check here, a canceled generation still
|
||||
// scattered variant files across the generated component dir and left them
|
||||
// there — the `stale_generation_epoch` returns below have no rollback.
|
||||
@@ -467,15 +443,6 @@ function readComponentPublicationTarget(manifestPath, cwd, id) {
|
||||
return { manifest, manifestPath, sourcePath, componentPath };
|
||||
}
|
||||
|
||||
function readSourceArtifactPublicationTarget(requestedPath, cwd, id) {
|
||||
const manifest = findSourceArtifactManifest(id, cwd);
|
||||
if (!manifest) return null;
|
||||
if (path.resolve(requestedPath) !== path.resolve(manifest.previewPath)) {
|
||||
return failure('source_artifact_preview_mismatch');
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function componentManifestMismatch(target, artifact) {
|
||||
for (const field of COMPONENT_MANIFEST_FIELDS) {
|
||||
if (JSON.stringify(target[field] ?? null) !== JSON.stringify(artifact[field] ?? null)) return field;
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { getLiveDir, safeSessionId } from '../lib/impeccable-paths.mjs';
|
||||
|
||||
export const SOURCE_ARTIFACT_PREVIEW_MODE = 'source-artifact';
|
||||
|
||||
export function scaffoldSourceArtifactSession({
|
||||
id,
|
||||
count,
|
||||
sourceFile,
|
||||
sourceStartLine,
|
||||
sourceEndLine,
|
||||
originalSource,
|
||||
previewContent,
|
||||
cwd = process.cwd(),
|
||||
} = {}) {
|
||||
safeSessionId(id);
|
||||
const sourcePath = resolveInside(cwd, sourceFile);
|
||||
if (!sourcePath || !fs.existsSync(sourcePath)) throw new Error('source artifact target missing');
|
||||
|
||||
const sessionDir = path.join(getLiveDir(cwd), 'previews', id);
|
||||
const extension = path.extname(sourcePath) || '.html';
|
||||
const previewPath = path.join(sessionDir, 'preview' + extension);
|
||||
const manifestPath = path.join(sessionDir, 'manifest.json');
|
||||
fs.mkdirSync(sessionDir, { recursive: true });
|
||||
|
||||
const manifest = {
|
||||
id,
|
||||
count: Number(count || 1),
|
||||
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
|
||||
sourceFile: relative(cwd, sourcePath),
|
||||
previewFile: relative(cwd, previewPath),
|
||||
sourceStartLine: Number(sourceStartLine),
|
||||
sourceEndLine: Number(sourceEndLine),
|
||||
originalSource: String(originalSource || ''),
|
||||
};
|
||||
fs.writeFileSync(previewPath, String(previewContent || ''), 'utf-8');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
||||
return { ...manifest, manifestFile: relative(cwd, manifestPath), sessionDir: relative(cwd, sessionDir) };
|
||||
}
|
||||
|
||||
export function findSourceArtifactManifest(id, cwd = process.cwd()) {
|
||||
try { safeSessionId(id); } catch { return null; }
|
||||
const manifestPath = path.join(getLiveDir(cwd), 'previews', id, 'manifest.json');
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); } catch { return null; }
|
||||
if (manifest?.id !== id || manifest?.previewMode !== SOURCE_ARTIFACT_PREVIEW_MODE) return null;
|
||||
const sourcePath = resolveInside(cwd, manifest.sourceFile);
|
||||
const previewPath = resolveInside(cwd, manifest.previewFile);
|
||||
if (!sourcePath || !previewPath || !fs.existsSync(sourcePath) || !fs.existsSync(previewPath)) return null;
|
||||
return { ...manifest, manifestPath, sourcePath, previewPath };
|
||||
}
|
||||
|
||||
export function removeSourceArtifactSession(id, cwd = process.cwd()) {
|
||||
try { safeSessionId(id); } catch { return false; }
|
||||
const sessionDir = path.join(getLiveDir(cwd), 'previews', id);
|
||||
if (!fs.existsSync(sessionDir)) return false;
|
||||
fs.rmSync(sessionDir, { recursive: true, force: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveInside(cwd, value) {
|
||||
if (!value || typeof value !== 'string') return null;
|
||||
const root = path.resolve(cwd);
|
||||
const resolved = path.resolve(root, value);
|
||||
const rel = path.relative(root, resolved);
|
||||
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function relative(cwd, value) {
|
||||
return path.relative(cwd, value).split(path.sep).join('/');
|
||||
}
|
||||
Reference in New Issue
Block a user