mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
Preserve experimental Live app-server workstream
Snapshot the current app-server implementation, shared Live optimizations, generated harness output, and in-progress site work before restoring polling as the primary runtime path. Prepared with Codex assistance under maintainer direction.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
selectFastCodexModel,
|
||||
selectLowestReasoningEffort,
|
||||
selectQualityCodexModel,
|
||||
} from './codex-app-server-client.mjs';
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
buildCodexWorkerInstructions,
|
||||
buildCodexWorkerTurnInputs,
|
||||
buildGenerationTurnInput,
|
||||
codexWorkerDetectorRepairSchema,
|
||||
codexWorkerOutputSchemaForPhase,
|
||||
codexWorkerStateIsOwned,
|
||||
generationIsCanceled,
|
||||
isCodexComponentPreviewMode,
|
||||
prepareCodexWorkerPhase,
|
||||
publishCodexWorkerPhase,
|
||||
readPreparedArtifact,
|
||||
@@ -36,6 +38,7 @@ import { createLiveSessionStore } from './session-store.mjs';
|
||||
|
||||
export const CODEX_WORKER_EVENT_TYPES = Object.freeze(['generate', 'accept', 'discard', 'prefetch']);
|
||||
export const CODEX_WORKER_EVENT_LEASE_MS = 15_000;
|
||||
const LOCAL_SCRIPTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
export class CodexLiveWorkerSupervisor {
|
||||
constructor({
|
||||
@@ -53,6 +56,7 @@ export class CodexLiveWorkerSupervisor {
|
||||
publishCheckpoint = postVariantCheckpoint,
|
||||
publishPhase = postAgentPhase,
|
||||
postCleanup = postCarbonizeCleanup,
|
||||
detectCandidate = detectPreparedArtifact,
|
||||
sessionStore = null,
|
||||
log = () => {},
|
||||
}) {
|
||||
@@ -70,6 +74,7 @@ export class CodexLiveWorkerSupervisor {
|
||||
this.publishCheckpoint = publishCheckpoint;
|
||||
this.publishPhase = publishPhase;
|
||||
this.postCleanup = postCleanup;
|
||||
this.detectCandidate = detectCandidate;
|
||||
this.sessionStore = sessionStore || createLiveSessionStore({ cwd: this.cwd });
|
||||
this.log = log;
|
||||
this.running = false;
|
||||
@@ -84,10 +89,11 @@ export class CodexLiveWorkerSupervisor {
|
||||
this.threadReady = Promise.resolve(null);
|
||||
this.model = null;
|
||||
this.liveSpec = '';
|
||||
this.threadPrimed = false;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.liveSpec = readOptional(path.join(this.scriptsDir, '..', 'reference', 'live.md'));
|
||||
this.liveSpec = readOptional(path.join(this.scriptsDir, '..', 'reference', 'live-generation.md'));
|
||||
await this.client.connect();
|
||||
const models = await this.client.listModels();
|
||||
this.model = this.config.model
|
||||
@@ -107,6 +113,7 @@ export class CodexLiveWorkerSupervisor {
|
||||
sandbox: 'read-only',
|
||||
baseInstructions: buildCodexWorkerInstructions(this.liveSpec),
|
||||
});
|
||||
this.threadPrimed = prior.threadPrimed === true;
|
||||
} catch (error) {
|
||||
this.log(`resume failed; creating replacement worker thread: ${error.message}`);
|
||||
}
|
||||
@@ -212,19 +219,22 @@ export class CodexLiveWorkerSupervisor {
|
||||
const snapshot = this.sessionStore.getSnapshot(event.id, { includeCompleted: true });
|
||||
const sameEpoch = Number(snapshot?.generationEpoch || 1) === Number(event.generationEpoch || 1);
|
||||
let arrivedVariants = sameEpoch ? Number(snapshot?.arrivedVariants || 0) : 0;
|
||||
let completedRemainder = false;
|
||||
if (this.config.delivery === 'progressive' && expectedVariants > 1) {
|
||||
if (arrivedVariants < 1) {
|
||||
await this.runGenerationPhase(event, 'first', 1);
|
||||
arrivedVariants = 1;
|
||||
}
|
||||
if (this.isCanceled(event.id)) return;
|
||||
if (expectedVariants > 2 && arrivedVariants < 2) {
|
||||
await this.runGenerationPhase(event, 'second', 2);
|
||||
arrivedVariants = 2;
|
||||
if (arrivedVariants < expectedVariants) {
|
||||
await this.runGenerationPhase(event, 'remainder', expectedVariants);
|
||||
arrivedVariants = expectedVariants;
|
||||
completedRemainder = true;
|
||||
}
|
||||
if (this.isCanceled(event.id)) return;
|
||||
if (arrivedVariants < expectedVariants) {
|
||||
await this.runGenerationPhase(event, 'final', expectedVariants);
|
||||
const latest = this.sessionStore.getSnapshot(event.id, { includeCompleted: true });
|
||||
if (!completedRemainder && arrivedVariants >= expectedVariants && latest?.paramsPublished !== true) {
|
||||
await this.runGenerationPhase(event, 'params', expectedVariants);
|
||||
}
|
||||
} else if (arrivedVariants < expectedVariants) {
|
||||
await this.runGenerationPhase(event, 'atomic', expectedVariants);
|
||||
@@ -245,6 +255,7 @@ export class CodexLiveWorkerSupervisor {
|
||||
}
|
||||
|
||||
startWorkerThread() {
|
||||
this.threadPrimed = false;
|
||||
return this.client.startDedicatedThread({
|
||||
model: this.model.model || this.model.id,
|
||||
cwd: this.cwd,
|
||||
@@ -311,7 +322,9 @@ export class CodexLiveWorkerSupervisor {
|
||||
cwd: this.cwd,
|
||||
maxBytes: this.config.maxArtifactBytes,
|
||||
});
|
||||
const contexts = readGenerationContexts(this.cwd, this.scriptsDir, event);
|
||||
const contexts = readGenerationContexts(this.cwd, this.scriptsDir, event, {
|
||||
includeStable: !this.threadPrimed,
|
||||
});
|
||||
const prompt = buildGenerationTurnInput({
|
||||
event,
|
||||
phase,
|
||||
@@ -322,108 +335,146 @@ export class CodexLiveWorkerSupervisor {
|
||||
});
|
||||
const input = buildCodexWorkerTurnInputs({
|
||||
prompt,
|
||||
skillPath: resolveCodexWorkerSkillPath(this.scriptsDir),
|
||||
skillPath: this.threadPrimed ? null : resolveCodexWorkerSkillPath(this.scriptsDir),
|
||||
screenshotPath: event.screenshotPath,
|
||||
cwd: this.cwd,
|
||||
});
|
||||
let publishedFromMessage = false;
|
||||
let publicationPromise = null;
|
||||
let earlyCandidateError = null;
|
||||
let durableCandidate = null;
|
||||
const publishCandidate = async (answer) => {
|
||||
if (publishedFromMessage || this.isCanceled(event.id)) return;
|
||||
if (!publicationPromise) {
|
||||
publicationPromise = (async () => {
|
||||
await this.publishPhase(this.base, this.token, {
|
||||
eventId: event.id,
|
||||
phase: generationPhaseName(phase, 'validating'),
|
||||
durationMs: Date.now() - phaseStartedAt,
|
||||
});
|
||||
if (!durableCandidate) {
|
||||
const candidatePath = path.resolve(this.cwd, prepared.artifactFile);
|
||||
if (!prepared.previewMode && (phase === 'first' || phase === 'second' || phase === 'final')) {
|
||||
// A structured agent message and the final turn result can contain
|
||||
// the same delta. Always apply against the immutable phase input so
|
||||
// a failed publication/checkpoint retry cannot double-insert it.
|
||||
fs.writeFileSync(candidatePath, artifact.content, 'utf-8');
|
||||
}
|
||||
const applied = applyCodexWorkerOutput({
|
||||
output: answer,
|
||||
prepared,
|
||||
phase,
|
||||
expectedVariants: Number(event.count || arrivedVariants),
|
||||
sessionId: event.id,
|
||||
scaffold: event.scaffold,
|
||||
cwd: this.cwd,
|
||||
maxBytes: this.config.maxArtifactBytes,
|
||||
});
|
||||
if (!prepared.previewMode && !applied.sourceDelta && (phase === 'second' || phase === 'final')) {
|
||||
const reconciled = reconcilePublishedSourceVariants({
|
||||
current: artifact.content,
|
||||
candidate: fs.readFileSync(candidatePath, 'utf-8'),
|
||||
priorArrived: Math.max(1, arrivedVariants - 1),
|
||||
});
|
||||
if (!reconciled.ok) throw supervisorError(`reconcile_${reconciled.error}`);
|
||||
fs.writeFileSync(candidatePath, reconciled.content, 'utf-8');
|
||||
}
|
||||
if (this.isCanceled(event.id)) return;
|
||||
const published = publishCodexWorkerPhase({ event, prepared, arrivedVariants, cwd: this.cwd });
|
||||
durableCandidate = { applied, published, planRecorded: false };
|
||||
}
|
||||
if (durableCandidate.applied.plan && !durableCandidate.planRecorded) {
|
||||
this.sessionStore.appendEvent({
|
||||
type: 'variant_plan',
|
||||
id: event.id,
|
||||
plan: durableCandidate.applied.plan,
|
||||
});
|
||||
durableCandidate.planRecorded = true;
|
||||
}
|
||||
if (this.isCanceled(event.id)) return;
|
||||
await this.publishCheckpoint(this.base, this.token, {
|
||||
event,
|
||||
published: durableCandidate.published,
|
||||
scaffold: event.scaffold,
|
||||
arrivedVariants,
|
||||
});
|
||||
publishedFromMessage = true;
|
||||
})();
|
||||
}
|
||||
const pendingPublication = publicationPromise;
|
||||
try {
|
||||
await pendingPublication;
|
||||
} catch (error) {
|
||||
if (!earlyCandidateError) earlyCandidateError = error;
|
||||
} finally {
|
||||
if (publicationPromise === pendingPublication) publicationPromise = null;
|
||||
}
|
||||
};
|
||||
if (this.isCanceled(event.id)) return;
|
||||
const result = await this.runTurnWithReconnect({
|
||||
const outputSchema = codexWorkerOutputSchemaForPhase(
|
||||
phase,
|
||||
Number(event.count || arrivedVariants),
|
||||
{ sourceDelta: (phase === 'first' || phase === 'remainder' || phase === 'params') && !isCodexComponentPreviewMode(prepared.previewMode) },
|
||||
);
|
||||
let result = await this.runTurnWithReconnect({
|
||||
input,
|
||||
outputSchema: codexWorkerOutputSchemaForPhase(
|
||||
phase,
|
||||
Number(event.count || arrivedVariants),
|
||||
{ sourceDelta: (phase === 'first' || phase === 'second' || phase === 'final') && !prepared.previewMode },
|
||||
),
|
||||
onAgentMessage: publishCandidate,
|
||||
outputSchema,
|
||||
eventId: event.id,
|
||||
effort: phase === 'params' ? 'low' : undefined,
|
||||
});
|
||||
this.threadPrimed = true;
|
||||
this.writeState('working', { eventId: event.id });
|
||||
if (this.isCanceled(event.id)) return;
|
||||
if (!publishedFromMessage) await publishCandidate(result.answer);
|
||||
if (!publishedFromMessage) throw earlyCandidateError || supervisorError('worker_output_not_published');
|
||||
await this.publishPhase(this.base, this.token, {
|
||||
eventId: event.id,
|
||||
phase: generationPhaseName(phase, 'validating'),
|
||||
durationMs: Date.now() - phaseStartedAt,
|
||||
});
|
||||
|
||||
const baselineFindings = this.detectCandidate(prepared, {
|
||||
cwd: this.cwd,
|
||||
scriptsDir: this.scriptsDir,
|
||||
});
|
||||
let applied;
|
||||
let newFindings;
|
||||
let acceptedDetectorWaivers = [];
|
||||
for (let repairAttempt = 0; repairAttempt <= 1; repairAttempt += 1) {
|
||||
restorePreparedArtifact(prepared, artifact, { cwd: this.cwd });
|
||||
applied = applyCodexWorkerOutput({
|
||||
output: result.answer,
|
||||
prepared,
|
||||
phase,
|
||||
expectedVariants: Number(event.count || arrivedVariants),
|
||||
sessionId: event.id,
|
||||
scaffold: event.scaffold,
|
||||
cwd: this.cwd,
|
||||
maxBytes: this.config.maxArtifactBytes,
|
||||
});
|
||||
reconcileCandidateIfNeeded({
|
||||
applied,
|
||||
artifact,
|
||||
prepared,
|
||||
phase,
|
||||
arrivedVariants,
|
||||
cwd: this.cwd,
|
||||
});
|
||||
newFindings = diffDetectorFindings(
|
||||
baselineFindings,
|
||||
this.detectCandidate(prepared, { cwd: this.cwd, scriptsDir: this.scriptsDir }),
|
||||
);
|
||||
const waiverResolution = resolveDetectorFindingWaivers(
|
||||
newFindings,
|
||||
extractDetectorWaivers(result.answer),
|
||||
);
|
||||
newFindings = waiverResolution.unresolved;
|
||||
acceptedDetectorWaivers = waiverResolution.accepted;
|
||||
if (newFindings.length === 0) break;
|
||||
if (repairAttempt === 1) {
|
||||
const error = supervisorError('worker_output_detector_findings');
|
||||
error.findings = newFindings;
|
||||
throw error;
|
||||
}
|
||||
restorePreparedArtifact(prepared, artifact, { cwd: this.cwd });
|
||||
result = await this.runTurnWithReconnect({
|
||||
input: buildCodexWorkerTurnInputs({
|
||||
prompt: buildDetectorRepairPrompt(phase, newFindings),
|
||||
cwd: this.cwd,
|
||||
}),
|
||||
outputSchema: codexWorkerDetectorRepairSchema(outputSchema),
|
||||
eventId: event.id,
|
||||
});
|
||||
if (this.isCanceled(event.id)) return;
|
||||
}
|
||||
|
||||
if (applied.plan) {
|
||||
this.sessionStore.appendEvent({
|
||||
type: 'variant_plan',
|
||||
id: event.id,
|
||||
plan: applied.plan,
|
||||
});
|
||||
}
|
||||
if (acceptedDetectorWaivers.length > 0) {
|
||||
this.sessionStore.appendEvent({
|
||||
type: 'detector_waivers',
|
||||
id: event.id,
|
||||
phase,
|
||||
waivers: acceptedDetectorWaivers.map(({ waiver }) => waiver),
|
||||
});
|
||||
}
|
||||
if (this.isCanceled(event.id)) return;
|
||||
const published = publishCodexWorkerPhase({ event, prepared, arrivedVariants, phase, cwd: this.cwd });
|
||||
let checkpointError;
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
try {
|
||||
await this.publishCheckpoint(this.base, this.token, {
|
||||
event,
|
||||
published,
|
||||
scaffold: event.scaffold,
|
||||
arrivedVariants,
|
||||
});
|
||||
checkpointError = null;
|
||||
break;
|
||||
} catch (error) {
|
||||
checkpointError = error;
|
||||
}
|
||||
}
|
||||
if (checkpointError) throw checkpointError;
|
||||
if (['remainder', 'params', 'atomic'].includes(phase)) {
|
||||
await this.publishPhase(this.base, this.token, {
|
||||
eventId: event.id,
|
||||
phase: 'parameters_ready',
|
||||
durationMs: Date.now() - phaseStartedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async runTurnWithReconnect({ input, outputSchema, onAgentMessage, eventId = this.active?.eventId }) {
|
||||
async runTurnWithReconnect({
|
||||
input,
|
||||
outputSchema,
|
||||
onAgentMessage,
|
||||
eventId = this.active?.eventId,
|
||||
effort,
|
||||
}) {
|
||||
let firstError;
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
try {
|
||||
const threadId = this.thread.id;
|
||||
if (this.active?.eventId === eventId) this.active.threadId = threadId;
|
||||
const turn = await this.client.startTurn({
|
||||
threadId,
|
||||
input,
|
||||
cwd: this.cwd,
|
||||
model: this.model.model || this.model.id,
|
||||
effort: preferredEffort(this.model, this.config.effort),
|
||||
effort: preferredEffort(this.model, effort || this.config.effort),
|
||||
summary: 'none',
|
||||
approvalPolicy: 'never',
|
||||
sandboxPolicy: { type: 'readOnly' },
|
||||
@@ -449,17 +500,26 @@ export class CodexLiveWorkerSupervisor {
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
this.thread = await this.client.reconnect({
|
||||
threadId: this.thread.id,
|
||||
this.thread = await this.reconnectThread(this.thread, this.model);
|
||||
this.writeState('ready', { reconnectedAt: new Date().toISOString() });
|
||||
}
|
||||
|
||||
async reconnectThread(thread, model = this.model) {
|
||||
const resumed = await this.client.reconnect({
|
||||
threadId: thread.id,
|
||||
resumeParams: {
|
||||
model: this.model.model || this.model.id,
|
||||
model: model.model || model.id,
|
||||
cwd: this.cwd,
|
||||
approvalPolicy: 'never',
|
||||
sandbox: 'read-only',
|
||||
baseInstructions: buildCodexWorkerInstructions(this.liveSpec),
|
||||
},
|
||||
});
|
||||
this.writeState('ready', { reconnectedAt: new Date().toISOString() });
|
||||
if (thread === this.thread) {
|
||||
this.thread = resumed;
|
||||
this.writeState('ready', { reconnectedAt: new Date().toISOString() });
|
||||
}
|
||||
return resumed;
|
||||
}
|
||||
|
||||
async cancelActive(reason, eventId = null) {
|
||||
@@ -547,6 +607,7 @@ export class CodexLiveWorkerSupervisor {
|
||||
effort: this.model ? preferredEffort(this.model, this.config.effort) : this.config.effort,
|
||||
profile: this.config.profile,
|
||||
delivery: this.config.delivery,
|
||||
threadPrimed: this.threadPrimed,
|
||||
eventId: this.active?.eventId || null,
|
||||
};
|
||||
}
|
||||
@@ -565,7 +626,7 @@ export class CodexLiveWorkerSupervisor {
|
||||
|
||||
function generationPhaseName(phase, state) {
|
||||
if (phase === 'first') return `first_variant_${state}`;
|
||||
if (phase === 'second') return `second_variant_${state}`;
|
||||
if (phase === 'params') return `variant_parameters_${state}`;
|
||||
return `remaining_variants_${state}`;
|
||||
}
|
||||
|
||||
@@ -591,6 +652,7 @@ export async function postVariantCheckpoint(base, token, {
|
||||
type: 'checkpoint',
|
||||
id: event.id,
|
||||
revision: published.revision,
|
||||
revisionDomain: 'publication',
|
||||
phase: 'cycling',
|
||||
reason: 'variants_progress',
|
||||
arrivedVariants,
|
||||
@@ -598,6 +660,7 @@ export async function postVariantCheckpoint(base, token, {
|
||||
sourceFile: scaffold.sourceFile || scaffold.file,
|
||||
previewFile: scaffold.file,
|
||||
previewMode: scaffold.previewMode || 'source',
|
||||
publicationKind: published.publicationKind || 'variants',
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw supervisorError(`checkpoint_${response.status}`);
|
||||
@@ -652,6 +715,7 @@ export function buildDeterministicScaffoldCommand(event, scriptsDir) {
|
||||
const script = path.join(scriptsDir, insert ? 'live-insert.mjs' : 'live-wrap.mjs');
|
||||
const args = ['--id', String(event.id), '--count', String(event.count || 3)];
|
||||
const target = insert ? event.insert?.anchor || {} : event.element || {};
|
||||
if (!insert) args.push('--isolated');
|
||||
if (insert) args.push('--position', String(event.insert?.position || 'after'));
|
||||
if (target.id) args.push('--element-id', String(target.id));
|
||||
const classes = Array.isArray(target.classes) ? target.classes.join(',') : target.className;
|
||||
@@ -687,84 +751,195 @@ export function runDeterministicScaffold(event, {
|
||||
return scaffold;
|
||||
}
|
||||
|
||||
function readGenerationContexts(cwd, scriptsDir, event) {
|
||||
function restorePreparedArtifact(prepared, artifact, { cwd }) {
|
||||
if (!isCodexComponentPreviewMode(prepared.previewMode)) {
|
||||
fs.writeFileSync(path.resolve(cwd, prepared.artifactFile), artifact.content, 'utf-8');
|
||||
return;
|
||||
}
|
||||
const componentDir = path.resolve(cwd, prepared.componentDir);
|
||||
fs.mkdirSync(componentDir, { recursive: true });
|
||||
for (const name of fs.readdirSync(componentDir)) {
|
||||
if (/^(?:v\d+\.(?:svelte|vue)|params\.json)$/.test(name)) {
|
||||
fs.unlinkSync(path.join(componentDir, name));
|
||||
}
|
||||
}
|
||||
for (const [name, content] of Object.entries(artifact.files || {})) {
|
||||
fs.writeFileSync(path.join(componentDir, name), content, 'utf-8');
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.resolve(cwd, prepared.artifactFile),
|
||||
JSON.stringify(artifact.manifest, null, 2) + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
function reconcileCandidateIfNeeded({ applied, artifact, prepared, phase, arrivedVariants, cwd }) {
|
||||
if (isCodexComponentPreviewMode(prepared.previewMode) || applied.sourceDelta || phase !== 'remainder') return;
|
||||
const candidatePath = path.resolve(cwd, prepared.artifactFile);
|
||||
const reconciled = reconcilePublishedSourceVariants({
|
||||
current: artifact.content,
|
||||
candidate: fs.readFileSync(candidatePath, 'utf-8'),
|
||||
priorArrived: Math.max(1, arrivedVariants - 1),
|
||||
});
|
||||
if (!reconciled.ok) throw supervisorError(`reconcile_${reconciled.error}`);
|
||||
fs.writeFileSync(candidatePath, reconciled.content, 'utf-8');
|
||||
}
|
||||
|
||||
export function detectPreparedArtifact(prepared, {
|
||||
cwd = process.cwd(),
|
||||
scriptsDir = LOCAL_SCRIPTS_DIR,
|
||||
spawn = spawnSync,
|
||||
} = {}) {
|
||||
const targets = detectorTargets(prepared, cwd);
|
||||
if (targets.length === 0) return [];
|
||||
const detectorScript = [
|
||||
path.join(scriptsDir, 'detect.mjs'),
|
||||
path.join(LOCAL_SCRIPTS_DIR, 'detect.mjs'),
|
||||
].find((candidate) => fs.existsSync(candidate));
|
||||
if (!detectorScript) throw supervisorError('codex_worker_detector_unavailable');
|
||||
const result = spawn(process.execPath, [detectorScript, '--json', ...targets], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
});
|
||||
if (result.error) throw supervisorError(`codex_worker_detector_failed:${result.error.message}`);
|
||||
try {
|
||||
const findings = JSON.parse(String(result.stdout || '[]'));
|
||||
if (!Array.isArray(findings)) throw new Error('expected findings array');
|
||||
return findings;
|
||||
} catch (error) {
|
||||
throw supervisorError(`codex_worker_detector_invalid:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function detectorTargets(prepared, cwd) {
|
||||
if (!isCodexComponentPreviewMode(prepared.previewMode)) {
|
||||
return [path.resolve(cwd, prepared.artifactFile)];
|
||||
}
|
||||
const componentDir = path.resolve(cwd, prepared.componentDir);
|
||||
try {
|
||||
return fs.readdirSync(componentDir)
|
||||
.filter((name) => /\.(?:vue|svelte)$/.test(name))
|
||||
.map((name) => path.join(componentDir, name));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function diffDetectorFindings(before, after) {
|
||||
const remaining = new Map();
|
||||
for (const finding of before || []) {
|
||||
const key = detectorFindingKey(finding);
|
||||
remaining.set(key, (remaining.get(key) || 0) + 1);
|
||||
}
|
||||
const added = [];
|
||||
for (const finding of after || []) {
|
||||
const key = detectorFindingKey(finding);
|
||||
const count = remaining.get(key) || 0;
|
||||
if (count > 0) remaining.set(key, count - 1);
|
||||
else added.push(finding);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
function detectorFindingKey(finding) {
|
||||
return [
|
||||
path.basename(String(finding?.file || '')),
|
||||
finding?.antipattern || finding?.id || '',
|
||||
finding?.snippet || '',
|
||||
finding?.ignoreValue || '',
|
||||
].join('\u0000');
|
||||
}
|
||||
|
||||
export function buildDetectorRepairPrompt(phase, findings) {
|
||||
return [
|
||||
`The candidate for Live phase ${phase} has new Impeccable detector findings.`,
|
||||
'Use design judgment on every finding. Fix real defects. If a finding is contextually intentional or a detector false positive, leave that design intact and add one narrow detectorWaivers entry copied from the finding with a concrete reason. Return detectorWaivers as an empty array when every finding was fixed. Every finding must either disappear on the next scan or match an explicit waiver; unresolved findings still block publication.',
|
||||
'Return the complete replacement JSON for the same phase and schema. Do not explain, call tools, persist project detector config, add inline ignore comments, or alter immutable variants.',
|
||||
'<detector_findings>',
|
||||
JSON.stringify((findings || []).slice(0, 40).map((finding) => ({
|
||||
rule: finding.antipattern || finding.id,
|
||||
name: finding.name,
|
||||
description: finding.description,
|
||||
severity: finding.severity,
|
||||
snippet: finding.snippet,
|
||||
file: path.basename(String(finding.file || '')),
|
||||
ignoreValue: finding.ignoreValue || '',
|
||||
})), null, 2),
|
||||
'</detector_findings>',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function resolveDetectorFindingWaivers(findings, waivers) {
|
||||
const candidates = (Array.isArray(waivers) ? waivers : [])
|
||||
.map(normalizeDetectorWaiver)
|
||||
.filter(Boolean);
|
||||
const accepted = [];
|
||||
const unresolved = [];
|
||||
for (const finding of findings || []) {
|
||||
const waiver = candidates.find((candidate) => detectorWaiverMatches(candidate, finding));
|
||||
if (waiver) accepted.push({ finding, waiver });
|
||||
else unresolved.push(finding);
|
||||
}
|
||||
return { accepted, unresolved };
|
||||
}
|
||||
|
||||
function extractDetectorWaivers(output) {
|
||||
try {
|
||||
const parsed = typeof output === 'string' ? JSON.parse(output) : output;
|
||||
return Array.isArray(parsed?.detectorWaivers) ? parsed.detectorWaivers : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDetectorWaiver(waiver) {
|
||||
if (!waiver || typeof waiver !== 'object') return null;
|
||||
const normalized = {
|
||||
rule: String(waiver.rule || '').trim().toLowerCase(),
|
||||
file: path.basename(String(waiver.file || '').trim()),
|
||||
snippet: String(waiver.snippet || '').trim(),
|
||||
ignoreValue: String(waiver.ignoreValue || '').trim(),
|
||||
reason: String(waiver.reason || '').trim(),
|
||||
};
|
||||
return normalized.rule && normalized.reason && (normalized.snippet || normalized.ignoreValue)
|
||||
? normalized
|
||||
: null;
|
||||
}
|
||||
|
||||
function detectorWaiverMatches(waiver, finding) {
|
||||
const rule = String(finding?.antipattern || finding?.id || '').trim().toLowerCase();
|
||||
const file = path.basename(String(finding?.file || '').trim());
|
||||
const snippet = String(finding?.snippet || '').trim();
|
||||
const ignoreValue = String(finding?.ignoreValue || '').trim();
|
||||
if (waiver.rule !== rule) return false;
|
||||
if (waiver.file && waiver.file !== file) return false;
|
||||
if (waiver.ignoreValue) return waiver.ignoreValue === ignoreValue;
|
||||
return Boolean(waiver.snippet && waiver.snippet === snippet);
|
||||
}
|
||||
|
||||
function readGenerationContexts(cwd, scriptsDir, event, { includeStable = true } = {}) {
|
||||
const context = loadContext(cwd);
|
||||
const action = event?.action;
|
||||
const safeAction = typeof action === 'string' && /^[a-z-]+$/.test(action) && action !== 'impeccable'
|
||||
? action
|
||||
: null;
|
||||
return {
|
||||
product: context.product || '',
|
||||
design: context.design || '',
|
||||
product: includeStable ? context.product || '' : '',
|
||||
design: includeStable ? context.design || '' : '',
|
||||
actionReference: safeAction
|
||||
? readOptional(path.join(scriptsDir, '..', 'reference', `${safeAction}.md`))
|
||||
: '',
|
||||
contextMetadata: {
|
||||
contextMetadata: includeStable ? {
|
||||
productPath: context.productPath,
|
||||
designPath: context.designPath,
|
||||
projectRoot: context.projectRoot,
|
||||
repoRoot: context.repoRoot,
|
||||
isMonorepo: context.isMonorepo,
|
||||
},
|
||||
sourceNeighborhood: readSourceNeighborhood(cwd, context.projectRoot, event?.scaffold?.sourceFile || event?.scaffold?.file),
|
||||
} : {},
|
||||
};
|
||||
}
|
||||
|
||||
function readSourceNeighborhood(cwd, projectRoot, sourceFile) {
|
||||
const roots = [projectRoot, cwd].filter(Boolean).map((value) => path.resolve(value));
|
||||
const result = {};
|
||||
let totalBytes = 0;
|
||||
const maxBytes = 180_000;
|
||||
const candidateNames = [
|
||||
sourceFile,
|
||||
'package.json',
|
||||
'src/styles.css',
|
||||
'src/index.css',
|
||||
'src/globals.css',
|
||||
'app/globals.css',
|
||||
'styles/globals.css',
|
||||
'tailwind.config.js',
|
||||
'tailwind.config.ts',
|
||||
].filter(Boolean);
|
||||
if (sourceFile) {
|
||||
for (const root of roots) {
|
||||
const source = readOptional(path.join(root, sourceFile));
|
||||
for (const specifier of localImportSpecifiers(source)) {
|
||||
const base = path.join(path.dirname(sourceFile), specifier);
|
||||
for (const suffix of ['', '.js', '.jsx', '.ts', '.tsx', '.css', '/index.js', '/index.jsx', '/index.ts', '/index.tsx']) {
|
||||
const candidate = `${base}${suffix}`.split(path.sep).join('/');
|
||||
if (fs.existsSync(path.join(root, candidate))) {
|
||||
candidateNames.push(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const root of roots) {
|
||||
for (const name of candidateNames) {
|
||||
if (Object.hasOwn(result, name)) continue;
|
||||
const file = path.join(root, name);
|
||||
const body = readOptional(file);
|
||||
if (!body) continue;
|
||||
const bytes = Buffer.byteLength(body);
|
||||
if (totalBytes + bytes > maxBytes) continue;
|
||||
result[name] = body;
|
||||
totalBytes += bytes;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function localImportSpecifiers(source) {
|
||||
if (!source) return [];
|
||||
const imports = [];
|
||||
const pattern = /(?:from\s*|import\s*)["'](\.{1,2}\/[^"']+)["']/g;
|
||||
let match;
|
||||
while ((match = pattern.exec(source))) imports.push(match[1]);
|
||||
return [...new Set(imports)];
|
||||
}
|
||||
|
||||
function readOptional(file) {
|
||||
try { return fs.readFileSync(file, 'utf-8'); } catch { return ''; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user