Files
pbakaus_impeccable/skill/scripts/live/generation-publisher.mjs
T
Paul BakausandClaude c7b67b3832 Drop the live generator subagent; fix the artifact decoy that broke accept
The first real Claude Code Live run failed, and the subagent was not the cause.

Root cause: progressive publication stages each revision as
`.impeccable/live/artifacts/<id>-r<n>.<source-ext>`, nothing ever deleted them,
and findSessionFile's walker skipped only node_modules/.git/dist/build. It
searches src, app, pages, ... then `.`; a project whose source is not under one of
those (this repo's own site lives in site/pages/) falls through to the `.` walk,
where dot-directories sort before letters. So accept found the artifact instead of
the real file. Two outcomes, both reproduced: where isGeneratedFile returns true
it declines with mode: 'fallback' (what the run hit, after which the agent
hand-carbonized several hundred lines across three stylesheets, including
unrequested drive-by edits); where it returns false, accept writes the variant
into the throwaway artifact and reports handled: true while real source never
changes.

The E2E suite could not have caught this. Every fixture puts source under `src/`,
which is searched before the `.` walk can reach `.impeccable`. Five framework
fixtures and three progressive scenarios pass because of fixture layout, not
because the path works. I read that as evidence and shouldn't have.

- Never search `.impeccable`: it is Impeccable's own state, never project source.
- Retire a session's staged artifacts on accept/discard, so they cannot outlive
  the session and become a decoy for anything else that walks the tree.
- Regression tests use a site/pages layout with artifacts present. All three fail
  against the previous code.

Generator subagent removed, on both harnesses:
The parent must hand-compress the design system into the handoff, and compression
is lossy. Measured on the real run: a 6,826-char handoff carrying exactly one
token reference, after the parent had itself read kinpaku-tokens.css. The subagent
then spent 3 of its first 9 turns hunting DESIGN.md, gave up, and emitted 0
var(--token) uses and 22 raw oklch literals — violating its own spec's "Never
invent raw colors when tokens exist" — including a 1:1 gold-on-gold contrast bug.
Isolation is not a benefit here; knowing the design system is the job. Generation
stays in the main thread, which already holds the tokens and writes them from the
first byte, so carbonize is a move rather than a translation.

Copy edits keep their subagent: applying a known set of ops to a named file is
self-contained, so an isolated context costs nothing. That is the line.

Progressive delivery stays for Codex and Claude Code, main-thread driven. Claude
Code keeps the full benefit because its poll is a background task. Codex's poll
blocks the foreground, so with no subagent the user sees variant 1 early via HMR
but cannot accept it until the trio finishes; that is the cost of the
simplification and it is worth naming.

Prepared with AI assistance under maintainer direction.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 18:56:23 -07:00

638 lines
23 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
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');
}
/**
* Delete a session's staged revision artifacts.
*
* Nothing used to remove these, and they are the reason a Live accept could
* resolve to the wrong file: `<id>-r<n>.<source-ext>` carries the session marker,
* so it is a decoy for any marker search that walks the project. live-accept no
* longer searches `.impeccable`, but the artifacts should not outlive the session
* they belong to either. Called on accept and discard.
*/
export function removeGenerationArtifacts(id, cwd = process.cwd()) {
let removed = 0;
try { safeSessionId(id); } catch { return removed; }
const artifactDir = path.join(getLiveDir(cwd), 'artifacts');
let entries;
try { entries = fs.readdirSync(artifactDir); } catch { return removed; }
for (const name of entries) {
if (!name.startsWith(id + '-r')) continue;
try { fs.rmSync(path.join(artifactDir, name), { force: true }); removed += 1; } catch { /* best effort */ }
}
return removed;
}
export function prepareGenerationArtifact({ id, sourceFile, cwd = process.cwd() } = {}) {
if (!id) return failure('missing_session_id');
if (!sourceFile) return failure('missing_file');
const requestedPath = resolveInside(cwd, sourceFile);
if (!requestedPath || !fs.existsSync(requestedPath)) return failure(requestedPath ? 'source_missing' : 'path_outside_project');
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;
try {
return withSourceLockSync(sourcePath, 'generation-prepare:' + id, () => {
const store = createLiveSessionStore({ cwd, sessionId: id });
const snapshot = store.getSnapshot(id, { includeCompleted: true });
if (!snapshot?.updatedAt) return failure('session_missing');
if (snapshot.generationCanceled === true) {
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 revision = Number(snapshot.publishedRevision || 0) + 1;
const artifactDir = path.join(getLiveDir(cwd), 'artifacts');
if (componentTarget) {
return prepareComponentArtifact({
id,
revision,
snapshot,
source,
sourcePath,
requestedPath,
target: componentTarget,
artifactDir,
cwd,
});
}
const extension = path.extname(sourcePath) || '.html';
const artifactPath = path.join(artifactDir, id + '-r' + revision + extension);
fs.mkdirSync(artifactDir, { recursive: true });
fs.writeFileSync(artifactPath, artifactBase, 'utf-8');
return {
ok: true,
id,
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),
};
}, { cwd });
} catch (error) {
if (error?.code === 'SOURCE_LOCKED') return failure('source_locked');
return failure('prepare_failed', { message: error?.message || String(error) });
}
}
export function publishGenerationArtifact({
id,
epoch,
sourceFile,
artifactFile,
expectedSourceHash,
arrivedVariants,
expectedVariants,
publicationKind,
cwd = process.cwd(),
} = {}) {
if (!id) return failure('missing_session_id');
if (!Number.isInteger(epoch) || epoch < 1) return failure('invalid_generation_epoch');
if (!sourceFile || !artifactFile) return failure('missing_file');
if (publicationKind && !['variants', 'params'].includes(publicationKind)) {
return failure('invalid_publication_kind');
}
const requestedPath = resolveInside(cwd, sourceFile);
const artifactPath = resolveInside(cwd, artifactFile);
if (!requestedPath || !artifactPath) return failure('path_outside_project');
if (!fs.existsSync(requestedPath)) return failure('source_missing');
if (!fs.existsSync(artifactPath)) return failure('artifact_missing');
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) {
return failure('artifact_preview_mode_mismatch');
}
if (componentTarget && componentTarget.manifest.previewMode !== artifactManifest?.previewMode) {
return failure('artifact_preview_mode_mismatch');
}
const sourcePath = componentTarget?.sourcePath || sourceArtifactTarget?.sourcePath || requestedPath;
try {
return withSourceLockSync(sourcePath, 'generation:' + id + ':' + epoch, () => {
const store = createLiveSessionStore({ cwd, sessionId: id });
const snapshot = store.getSnapshot(id, { includeCompleted: true });
if (!snapshot?.updatedAt) return failure('session_missing');
const stale = staleGenerationFailure(snapshot, epoch);
if (stale) return stale;
const current = fs.readFileSync(sourcePath, 'utf-8');
const currentHash = sha256(current);
if (!expectedSourceHash || currentHash !== expectedSourceHash) {
return failure('source_hash_mismatch', { actualSourceHash: currentHash });
}
if (componentTarget) {
return publishComponentArtifact({
id,
epoch,
snapshot,
target: componentTarget,
artifactManifest,
artifactPath,
sourcePath,
arrivedVariants,
expectedVariants,
publicationKind,
store,
cwd,
});
}
const stablePreview = sourceArtifactTarget
? fs.readFileSync(sourceArtifactTarget.previewPath, 'utf-8')
: current;
const artifact = fs.readFileSync(artifactPath, 'utf-8');
if (!artifact.includes('data-impeccable-variants="' + id + '"')) {
return failure('artifact_missing_session_wrapper');
}
const delivered = countDeliveredVariants(artifact);
if (delivered < 1) return failure('artifact_has_no_variants');
if (Number.isInteger(arrivedVariants) && delivered < arrivedVariants) {
return failure('artifact_variant_count_mismatch', { delivered });
}
const priorArrived = Math.max(0, Number(snapshot.arrivedVariants || 0));
for (let variant = 1; variant <= priorArrived; variant++) {
const currentVariant = extractVariantBlock(stablePreview, variant);
const artifactVariant = extractVariantBlock(artifact, variant);
if (!currentVariant || !artifactVariant) {
return failure('published_variant_missing', { variant });
}
if (sha256(withoutVariantParams(currentVariant)) !== sha256(withoutVariantParams(artifactVariant))) {
return failure('published_variant_changed', { variant });
}
}
const currentPreviewCss = extractPreviewCss(stablePreview, id);
const artifactPreviewCss = extractPreviewCss(artifact, id);
if (priorArrived > 0 && currentPreviewCss && !artifactPreviewCss.startsWith(currentPreviewCss)) {
return failure('published_variant_css_changed');
}
const commitSnapshot = store.getSnapshot(id, { includeCompleted: true });
const commitStale = staleGenerationFailure(commitSnapshot, epoch);
if (commitStale) return commitStale;
const artifactHash = sha256(artifact);
const publishPath = sourceArtifactTarget?.previewPath || sourcePath;
atomicReplace(publishPath, artifact);
const revision = Number(commitSnapshot.publishedRevision || 0) + 1;
store.appendEvent({
type: 'variant_published',
id,
generationEpoch: epoch,
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',
at: Date.now(),
});
return {
ok: true,
id,
epoch,
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',
};
}, { cwd });
} catch (error) {
if (error?.code === 'SOURCE_LOCKED') return failure('source_locked');
return failure('publish_failed', { message: error?.message || String(error) });
}
}
function prepareComponentArtifact({
id,
revision,
snapshot,
source,
sourcePath,
requestedPath,
target,
artifactDir,
cwd,
}) {
const artifactComponentDir = path.join(
artifactDir,
id + '-r' + revision + '-' + target.manifest.previewMode + '-' + process.pid + '-' + Date.now(),
);
fs.mkdirSync(artifactComponentDir, { recursive: true });
copyDirectoryFiles(target.componentPath, artifactComponentDir);
const artifactPath = path.join(artifactComponentDir, 'manifest.json');
const artifactManifest = {
...target.manifest,
componentDir: relative(cwd, artifactComponentDir),
};
fs.writeFileSync(artifactPath, JSON.stringify(artifactManifest, null, 2) + '\n', 'utf-8');
return {
ok: true,
id,
epoch: Number(snapshot.generationEpoch || 1),
revision,
sourceFile: relative(cwd, requestedPath),
targetSourceFile: relative(cwd, sourcePath),
artifactFile: relative(cwd, artifactPath),
componentDir: relative(cwd, artifactComponentDir),
previewMode: target.manifest.previewMode,
expectedSourceHash: sha256(source),
};
}
function publishComponentArtifact({
id,
epoch,
snapshot,
target,
artifactManifest,
artifactPath,
sourcePath,
arrivedVariants,
expectedVariants,
publicationKind,
store,
cwd,
}) {
if (!artifactManifest || typeof artifactManifest !== 'object') {
return failure('artifact_manifest_invalid');
}
if (artifactManifest.id !== id || target.manifest.id !== id) {
return failure('artifact_session_mismatch');
}
const artifactComponentPath = resolveInside(cwd, artifactManifest.componentDir);
if (!artifactComponentPath || path.resolve(artifactComponentPath) !== path.dirname(artifactPath)) {
return failure('artifact_component_dir_mismatch');
}
if (!isDescendant(path.join(getLiveDir(cwd), 'artifacts'), artifactComponentPath)) {
return failure('artifact_not_staged');
}
const immutableMismatch = componentManifestMismatch(target.manifest, artifactManifest);
if (immutableMismatch) {
return failure('artifact_manifest_changed', { field: immutableMismatch });
}
const expected = Number(expectedVariants || target.manifest.count || snapshot.expectedVariants || 0);
const declared = optionalPositiveInteger(artifactManifest.arrivedVariants);
const delivered = Number.isInteger(arrivedVariants) ? arrivedVariants : declared;
if (!Number.isInteger(delivered) || delivered < 1) return failure('artifact_has_no_variants');
if (expected > 0 && delivered > expected) {
return failure('artifact_variant_count_mismatch', { delivered, expected });
}
if (declared !== null && declared !== delivered) {
return failure('artifact_variant_count_mismatch', { delivered: declared, expected: delivered });
}
const priorArrived = Math.max(
optionalPositiveInteger(target.manifest.arrivedVariants) || 0,
Number(snapshot.arrivedVariants || 0),
);
if (delivered < priorArrived) {
return failure('artifact_variant_count_regressed', { delivered, priorArrived });
}
const componentExtension = target.manifest.componentExtension
|| (target.manifest.previewMode === 'vue-component' ? 'vue' : 'svelte');
const variantContents = [];
for (let variant = 1; variant <= delivered; variant++) {
const artifactVariantPath = path.join(artifactComponentPath, 'v' + variant + '.' + componentExtension);
if (!regularFileInside(artifactComponentPath, artifactVariantPath)) {
return failure('artifact_variant_missing', { variant });
}
const content = fs.readFileSync(artifactVariantPath, 'utf-8');
if (!content.trim()) return failure('artifact_variant_empty', { variant });
const targetVariantPath = path.join(target.componentPath, 'v' + variant + '.' + componentExtension);
if (variant <= priorArrived && !regularFileInside(target.componentPath, targetVariantPath)) {
return failure('published_variant_missing', { variant });
}
if (variant <= priorArrived) {
const prior = fs.readFileSync(targetVariantPath, 'utf-8');
if (sha256(prior) !== sha256(content)) {
return failure('published_variant_changed', { variant });
}
}
variantContents.push({ variant, content, targetPath: targetVariantPath });
}
const artifactParamsPath = path.join(artifactComponentPath, 'params.json');
let paramsContent = null;
if (fs.existsSync(artifactParamsPath)) {
if (!regularFileInside(artifactComponentPath, artifactParamsPath)) {
return failure('artifact_params_invalid');
}
paramsContent = fs.readFileSync(artifactParamsPath, 'utf-8');
const params = parseJson(paramsContent);
if (!params || typeof params !== 'object' || Array.isArray(params)) {
return failure('artifact_params_invalid');
}
}
// Check the fence before writing anything. The prepare→publish gap is exactly
// where an Accept lands, and the source-artifact 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.
const preWriteStale = staleGenerationFailure(store.getSnapshot(id, { includeCompleted: true }), epoch);
if (preWriteStale) return preWriteStale;
// Components and optional params become reachable before the manifest
// advertises them. Committing the manifest last makes publication atomic
// from the browser's point of view while the source lock excludes Accept.
fs.mkdirSync(target.componentPath, { recursive: true });
for (const variant of variantContents) {
if (variant.variant > priorArrived) atomicReplace(variant.targetPath, variant.content);
}
if (paramsContent !== null) {
atomicReplace(path.join(target.componentPath, 'params.json'), paramsContent);
}
// Re-check immediately before the manifest: the manifest is what makes the
// variants visible to the browser, so this is the gate that actually matters.
const commitSnapshot = store.getSnapshot(id, { includeCompleted: true });
const commitStale = staleGenerationFailure(commitSnapshot, epoch);
if (commitStale) return commitStale;
const publishedManifest = {
...target.manifest,
componentDir: relative(cwd, target.componentPath),
arrivedVariants: delivered,
};
delete publishedManifest.manifestPath;
const manifestContent = JSON.stringify(publishedManifest, null, 2) + '\n';
atomicReplace(target.manifestPath, manifestContent);
const digest = digestComponentPublication(manifestContent, variantContents, paramsContent);
const revision = Number(snapshot.publishedRevision || 0) + 1;
const sourceFile = relative(cwd, sourcePath);
const previewFile = relative(cwd, target.manifestPath);
store.appendEvent({
type: 'variant_published',
id,
generationEpoch: epoch,
revision,
digest,
sourceFile,
previewFile,
previewMode: target.manifest.previewMode,
arrivedVariants: delivered,
expectedVariants: expected || delivered,
publicationKind: publicationKind || 'variants',
at: Date.now(),
});
return {
ok: true,
id,
epoch,
revision,
digest,
sourceFile,
previewFile,
previewMode: target.manifest.previewMode,
componentDir: relative(cwd, target.componentPath),
arrivedVariants: delivered,
expectedVariants: expected || delivered,
publicationKind: publicationKind || 'variants',
};
}
const COMPONENT_MANIFEST_FIELDS = [
'id',
'mode',
'previewMode',
'sourceFile',
'sourceStartLine',
'sourceEndLine',
'insertLine',
'position',
'anchorStartLine',
'anchorEndLine',
'count',
'propContract',
'originalMarkup',
'anchorMarkup',
'runtimeModule',
'componentModuleBase',
'framework',
'componentExtension',
];
function readComponentPublicationTarget(manifestPath, cwd, id) {
if (path.basename(manifestPath) !== 'manifest.json') return null;
const manifest = readJson(manifestPath);
if (!manifest || !isComponentPreviewMode(manifest.previewMode)) return null;
if (manifest.id !== id) return failure('artifact_session_mismatch');
const sourcePath = resolveInside(cwd, manifest.sourceFile);
const componentPath = resolveInside(cwd, manifest.componentDir);
if (!sourcePath || !componentPath) return failure('path_outside_project');
if (!fs.existsSync(sourcePath)) return failure('source_missing');
if (path.resolve(componentPath) !== path.dirname(manifestPath)) {
return failure('manifest_component_dir_mismatch');
}
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;
}
return null;
}
function isComponentPreviewMode(value) {
return value === 'svelte-component' || value === 'vue-component';
}
function copyDirectoryFiles(sourceDir, targetDir) {
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
if (!entry.isFile() || entry.isSymbolicLink()) continue;
fs.copyFileSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name));
}
}
function regularFileInside(root, file) {
const rel = path.relative(root, file);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false;
try {
return fs.lstatSync(file).isFile();
} catch {
return false;
}
}
function isDescendant(root, candidate) {
const rel = path.relative(root, candidate);
return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
}
function digestComponentPublication(manifestContent, variants, paramsContent) {
const hash = createHash('sha256');
hash.update(manifestContent);
for (const variant of variants) {
hash.update('\0v' + variant.variant + '\0');
hash.update(variant.content);
}
if (paramsContent !== null) hash.update('\0params\0' + paramsContent);
return hash.digest('hex');
}
function readJson(file) {
try {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch {
return null;
}
}
function parseJson(value) {
try {
return JSON.parse(value);
} catch {
return null;
}
}
function optionalPositiveInteger(value) {
const number = Number(value);
return Number.isInteger(number) && number > 0 ? number : null;
}
function countDeliveredVariants(source) {
const matches = source.match(/<div\b[^>]*\bdata-impeccable-variant=(?:"|')(?!original(?:"|'))[^"']+(?:"|')[^>]*>/g);
return matches?.length || 0;
}
function extractVariantBlock(source, variant) {
const open = /<div\b[^>]*>/gi;
let match;
let start = -1;
const attr = new RegExp("\\bdata-impeccable-variant=(?:\"" + variant + "\"|'" + variant + "')");
while ((match = open.exec(source))) {
if (attr.test(match[0])) {
start = match.index;
break;
}
}
if (start < 0) return null;
const token = /<div\b[^>]*\/\s*>|<div\b[^>]*>|<\/div\s*>/gi;
token.lastIndex = start;
let depth = 0;
while ((match = token.exec(source))) {
if (/^<\/div/i.test(match[0])) {
depth -= 1;
if (depth === 0) return source.slice(start, token.lastIndex);
} else if (!/\/\s*>$/.test(match[0])) {
depth += 1;
}
}
return null;
}
function withoutVariantParams(block) {
return String(block || '').replace(
/\sdata-impeccable-params=(?:"[^"]*"|'[^']*')/i,
'',
);
}
function extractPreviewCss(source, id) {
const escapedId = String(id).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const open = new RegExp("<style\\b[^>]*\\bdata-impeccable-css=(?:\"" + escapedId + "\"|'" + escapedId + "')[^>]*>", 'i');
const match = open.exec(source);
if (!match) return '';
const start = match.index + match[0].length;
const end = source.indexOf('</style>', start);
if (end < 0) return '';
return source.slice(start, end)
.replace(/^\s*\{\s*`\s*/, '')
.replace(/\s*`\s*\}\s*$/, '')
.trim();
}
function atomicReplace(target, content) {
let mode = 0o666;
try { mode = fs.statSync(target).mode; } catch {}
const temp = target + '.impeccable-publish-' + process.pid + '-' + Date.now();
try {
fs.writeFileSync(temp, content, { encoding: 'utf-8', mode });
fs.renameSync(temp, target);
} finally {
try { fs.unlinkSync(temp); } catch {}
}
}
function resolveInside(cwd, value) {
const resolved = path.resolve(cwd, value);
const rel = path.relative(cwd, resolved);
if (rel.startsWith('..') || path.isAbsolute(rel)) return null;
return resolved;
}
function relative(cwd, value) {
return path.relative(cwd, value).split(path.sep).join('/');
}
function failure(error, details = {}) {
return { ok: false, error, ...details };
}
/**
* The generation fence: has this session been canceled (Accept/Discard landed),
* or has a newer generation superseded this epoch? Returns a failure result to
* propagate, or null when the caller may proceed.
*/
function staleGenerationFailure(snapshot, epoch) {
if (snapshot?.generationCanceled === true) {
return failure('stale_generation_epoch', { canceled: true, phase: snapshot.phase });
}
if (Number(snapshot?.generationEpoch || 1) !== epoch) {
return failure('stale_generation_epoch', { expectedEpoch: snapshot?.generationEpoch || 1 });
}
return null;
}