Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/live/manual-apply.mjs
1import { randomUUID } from 'node:crypto';2import fs from 'node:fs';3import path from 'node:path';4import { getLiveDir } from '../lib/impeccable-paths.mjs';5import { readBuffer as readManualEditsBuffer } from './manual-edits-buffer.mjs';67const APPLY_EVENT_HARD_TIMEOUT_MS = Number(process.env.IMPECCABLE_LIVE_APPLY_EVENT_HARD_TIMEOUT_MS || 150_000);8const APPLY_EVENT_SOFT_DEADLINE_MS = Number(process.env.IMPECCABLE_LIVE_APPLY_EVENT_SOFT_DEADLINE_MS || 120_000);9const DEFAULT_MANUAL_EDIT_APPLY_CHUNK_SIZE = 3;10const MIN_MANUAL_EDIT_APPLY_CHUNK_SIZE = 1;11const MAX_MANUAL_EDIT_APPLY_CHUNK_SIZE = 20;12const MANUAL_APPLY_COMPACT_TEXT_LIMIT = 240;13const MANUAL_APPLY_COMPACT_NEARBY_LIMIT = 4;1415export function createManualApplyController({16pendingEvents,17pendingApplyDeferreds,18timedOutApplyIds,19enqueueEvent,20acknowledgePendingEvent,21flushPendingPolls,22recordManualEditActivity,23cwd = () => process.cwd(),24} = {}) {25const projectCwd = () => typeof cwd === 'function' ? cwd() : cwd || process.cwd();2627function tombstoneTimedOutApplyId(eventId, details = {}) {28if (!eventId) return;29timedOutApplyIds.set(eventId, details);30if (timedOutApplyIds.size <= 200) return;31const oldest = timedOutApplyIds.keys().next().value;32timedOutApplyIds.delete(oldest);33}3435function pushApplyEventAndWait(batch, pageUrl, chunk = null, repair = null) {36const cwdValue = projectCwd();37const eventId = randomUUID().replace(/-/g, '').slice(0, 8);38const evidencePath = writeManualApplyEvidence(eventId, batch, cwdValue);39const event = {40type: 'manual_edit_apply',41id: eventId,42pageUrl,43batch: compactManualApplyBatch(batch, cwdValue),44evidencePath,45agentAction: buildManualApplyAgentAction(eventId),46schemaVersion: 1,47deadlineMs: APPLY_EVENT_SOFT_DEADLINE_MS,48};49if (chunk) event.chunk = chunk;50if (repair) event.repair = repair;51const rollbackSnapshot = snapshotApplyEventFiles(batch, cwdValue);52recordManualEditActivity('manual_edit_apply_dispatched', {53id: eventId,54pageUrl,55chunk,56repair,57entryCount: Array.isArray(batch.entries) ? batch.entries.length : 0,58opCount: countManualApplyOps(batch),59fileCount: collectManualApplyFiles(batch, [], cwdValue).length,60});61return new Promise((resolve, reject) => {62const timer = setTimeout(() => {63pendingApplyDeferreds.delete(eventId);64tombstoneTimedOutApplyId(eventId, { batch, rollbackSnapshot, cwd: cwdValue });65acknowledgePendingEvent(eventId);66removeManualApplyEvidence(evidencePath, cwdValue);67recordManualEditActivity('manual_edit_apply_timeout', {68id: eventId,69pageUrl,70chunk,71entryCount: Array.isArray(batch.entries) ? batch.entries.length : 0,72opCount: countManualApplyOps(batch),73});74reject(new Error('chat_agent_timeout'));75}, APPLY_EVENT_HARD_TIMEOUT_MS);76pendingApplyDeferreds.set(eventId, { resolve, reject, timer, event, batch, pageUrl, rollbackSnapshot, cwd: cwdValue });77enqueueEvent(event);78});79}8081async function pushBatchInChunksAndWait(batch, pageUrl, context = {}) {82const repair = context?.repair || batch?.repair || null;83if (repair) return pushApplyEventAndWait(batch, pageUrl, null, repair);84const chunks = splitManualApplyBatch(batch, manualEditApplyChunkSize());85if (chunks.length <= 1) return pushApplyEventAndWait(batch, pageUrl);8687const expectedOpsByEntry = new Map();88for (const entry of batch?.entries || []) {89expectedOpsByEntry.set(entry.id, Array.isArray(entry.ops) ? entry.ops.length : 0);90}9192const appliedOpsByEntry = new Map();93const failedByEntry = new Map();94const files = new Set();95const notes = [];96let aborted = false;9798for (const chunk of chunks) {99if (aborted) {100markChunkEntriesFailed(failedByEntry, chunk, 'manual_edit_chunk_aborted');101continue;102}103104let result;105try {106result = normalizeApplyChunkResult(await pushApplyEventAndWait(chunk.batch, pageUrl, chunk.meta));107} catch (err) {108markChunkEntriesFailed(failedByEntry, chunk, err.message || 'chat_agent_error');109aborted = true;110continue;111}112113for (const file of result.files) files.add(file);114notes.push(...result.notes);115116const chunkFailedIds = new Set();117for (const item of result.failed) {118const entryId = item.entryId || item.id;119if (!entryId) continue;120chunkFailedIds.add(entryId);121if (!failedByEntry.has(entryId)) {122failedByEntry.set(entryId, {123entryId,124reason: item.reason || item.message || 'failed',125candidates: Array.isArray(item.candidates) ? item.candidates : [],126});127}128}129130if (result.status === 'error') {131markChunkEntriesFailed(failedByEntry, chunk, result.message || firstFailureReason(result) || 'chat_agent_error');132aborted = true;133continue;134}135136const reportedAppliedIds = new Set(result.appliedEntryIds);137for (const entryId of reportedAppliedIds) {138if (!chunk.entryIds.has(entryId) || chunkFailedIds.has(entryId)) continue;139appliedOpsByEntry.set(entryId, (appliedOpsByEntry.get(entryId) || 0) + (chunk.opCountsByEntry.get(entryId) || 0));140}141142for (const entryId of chunk.entryIds) {143if (reportedAppliedIds.has(entryId) || chunkFailedIds.has(entryId)) continue;144if (!failedByEntry.has(entryId)) {145failedByEntry.set(entryId, { entryId, reason: 'not_reported_applied', candidates: [] });146}147}148}149150const appliedEntryIds = [];151for (const [entryId, expectedOps] of expectedOpsByEntry.entries()) {152if (failedByEntry.has(entryId)) continue;153if ((appliedOpsByEntry.get(entryId) || 0) === expectedOps && expectedOps > 0) {154appliedEntryIds.push(entryId);155} else if (!failedByEntry.has(entryId)) {156failedByEntry.set(entryId, { entryId, reason: 'not_reported_applied', candidates: [] });157}158}159160const failed = [...failedByEntry.values()];161return {162status: failed.length === 0 ? 'done' : appliedEntryIds.length > 0 ? 'partial' : 'error',163appliedEntryIds,164failed,165files: [...files],166notes,167};168}169170function getDeferred(eventId) {171return pendingApplyDeferreds.get(eventId) || null;172}173174function hasTimedOutId(eventId) {175return timedOutApplyIds.has(eventId);176}177178function resolveDeferred(eventId, body) {179const deferred = pendingApplyDeferreds.get(eventId);180if (!deferred) return false;181pendingApplyDeferreds.delete(eventId);182clearTimeout(deferred.timer);183removeManualApplyEvidence(deferred.event?.evidencePath, deferred.cwd || projectCwd());184deferred.resolve(body);185return true;186}187188function rejectDeferred(eventId, reason) {189const deferred = pendingApplyDeferreds.get(eventId);190if (!deferred) return false;191pendingApplyDeferreds.delete(eventId);192clearTimeout(deferred.timer);193removeManualApplyEvidence(deferred.event?.evidencePath, deferred.cwd || projectCwd());194deferred.reject(new Error(reason || 'chat_agent_error'));195return true;196}197198function referencedManualApplyEvidencePaths(cwdValue = projectCwd()) {199const referenced = new Set();200const add = (event) => {201const fullPath = normalizeManualApplyEvidencePath(event?.evidencePath, cwdValue);202if (fullPath) referenced.add(fullPath);203};204for (const entry of pendingEvents) add(entry.event);205for (const deferred of pendingApplyDeferreds.values()) add(deferred.event);206return referenced;207}208209function pruneStaleEvidence(cwdValue = projectCwd()) {210const dir = manualApplyEvidenceDir(cwdValue);211if (!fs.existsSync(dir)) return [];212const referenced = referencedManualApplyEvidencePaths(cwdValue);213const removed = [];214for (const name of fs.readdirSync(dir)) {215if (!name.endsWith('.json')) continue;216const fullPath = path.join(dir, name);217if (referenced.has(fullPath)) continue;218try {219fs.unlinkSync(fullPath);220removed.push(fullPath);221} catch {222// Stale evidence cleanup is best-effort; Apply verification never relies223// on deleting these files.224}225}226return removed;227}228229function rollbackTimedOutReply(msg) {230const details = timedOutApplyIds.get(msg.id);231if (!details) return { rolledBackFiles: [], rollbackFailures: [] };232timedOutApplyIds.delete(msg.id);233return rollbackApplySnapshot(234details.batch,235details.rollbackSnapshot,236msg.data?.files || [],237'stale_manual_edit_apply_reply',238details.cwd || projectCwd(),239);240}241242function cancelPendingEvents(pageUrl, reason = 'manual_edit_discarded') {243const canceledById = new Map();244const shouldCancel = (event) => event?.type === 'manual_edit_apply' && (!pageUrl || event.pageUrl === pageUrl);245246for (let i = pendingEvents.length - 1; i >= 0; i -= 1) {247const event = pendingEvents[i]?.event;248if (!shouldCancel(event)) continue;249pendingEvents.splice(i, 1);250removeManualApplyEvidence(event.evidencePath, projectCwd());251canceledById.set(event.id, {252id: event.id,253pageUrl: event.pageUrl,254entryCount: event.batch?.entries?.length || 0,255});256}257258for (const [eventId, deferred] of [...pendingApplyDeferreds.entries()]) {259if (!shouldCancel(deferred.event)) continue;260pendingApplyDeferreds.delete(eventId);261clearTimeout(deferred.timer);262const cwdValue = deferred.cwd || projectCwd();263const rollback = rollbackApplySnapshot(deferred.batch, deferred.rollbackSnapshot, [], reason, cwdValue);264tombstoneTimedOutApplyId(eventId, {265batch: deferred.batch,266rollbackSnapshot: deferred.rollbackSnapshot,267reason,268cwd: cwdValue,269});270removeManualApplyEvidence(deferred.event?.evidencePath, cwdValue);271canceledById.set(eventId, {272id: eventId,273pageUrl: deferred.pageUrl,274entryCount: deferred.batch?.entries?.length || 0,275rolledBackFiles: rollback.rolledBackFiles,276rollbackFailures: rollback.rollbackFailures,277});278deferred.reject(new Error(reason));279}280281if (canceledById.size > 0) flushPendingPolls();282return [...canceledById.values()];283}284285return {286buildAgentAction: buildManualApplyAgentAction,287cancelPendingEvents,288clearTransaction: (transactionId = null) => clearManualApplyTransaction(projectCwd(), transactionId),289countOps: countManualApplyOps,290getDeferred,291hasTimedOutId,292pruneStaleEvidence,293pushBatchInChunksAndWait,294readTransaction: () => readManualApplyTransaction(projectCwd()),295rejectDeferred,296resolveDeferred,297rollbackTimedOutReply,298rollbackTransaction: (opts = {}) => rollbackManualApplyTransaction({299cwd: projectCwd(),300recordManualEditActivity,301...opts,302}),303summarizeEvent: (event = {}, batch = event.batch) => summarizeManualApplyEvent(event, batch, projectCwd()),304validateResultMessage: validateManualApplyResultMessage,305writeTransaction: (opts = {}) => writeManualApplyTransaction({ cwd: projectCwd(), ...opts }),306};307}308309export function manualEditApplyChunkSize(env = process.env) {310const raw = Number(env.IMPECCABLE_LIVE_MANUAL_EDIT_CHUNK_SIZE);311if (!Number.isFinite(raw)) return DEFAULT_MANUAL_EDIT_APPLY_CHUNK_SIZE;312const size = Math.trunc(raw);313return Math.max(MIN_MANUAL_EDIT_APPLY_CHUNK_SIZE, Math.min(MAX_MANUAL_EDIT_APPLY_CHUNK_SIZE, size));314}315316export function countManualApplyOps(entriesOrBatch) {317const entries = Array.isArray(entriesOrBatch)318? entriesOrBatch319: Array.isArray(entriesOrBatch?.entries) ? entriesOrBatch.entries : [];320let count = 0;321for (const entry of entries) count += Array.isArray(entry.ops) ? entry.ops.length : 0;322return count;323}324325export function writeManualApplyEvidence(eventId, batch, cwd = process.cwd()) {326const dir = manualApplyEvidenceDir(cwd);327fs.mkdirSync(dir, { recursive: true });328const evidencePath = path.join(dir, `${eventId}.json`);329fs.writeFileSync(evidencePath, JSON.stringify(batch, null, 2) + '\n', 'utf-8');330return evidencePath;331}332333export function manualApplyEvidenceDir(cwd = process.cwd()) {334return path.join(getLiveDir(cwd), 'manual-edit-evidence');335}336337export function normalizeManualApplyEvidencePath(evidencePath, cwd = process.cwd()) {338if (!evidencePath || typeof evidencePath !== 'string') return null;339const fullPath = path.isAbsolute(evidencePath) ? evidencePath : path.resolve(cwd, evidencePath);340const evidenceDir = manualApplyEvidenceDir(cwd);341const relative = path.relative(evidenceDir, fullPath);342if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;343if (path.extname(relative) !== '.json') return null;344return fullPath;345}346347export function removeManualApplyEvidence(evidencePath, cwd = process.cwd()) {348const fullPath = normalizeManualApplyEvidencePath(evidencePath, cwd);349if (!fullPath) return false;350try {351fs.unlinkSync(fullPath);352return true;353} catch {354return false;355}356}357358export function compactManualApplyBatch(batch = {}, cwd = process.cwd()) {359const entries = (batch.entries || []).map(compactManualApplyEntry);360const candidates = compactManualApplyCandidates(batch.candidates || [], cwd);361return {362version: batch.version,363pageUrl: batch.pageUrl || null,364count: batch.count,365entries,366ops: entries.flatMap((entry) => entry.ops.map((op) => ({ ...op, entryId: entry.id }))),367candidates: candidates.length > 0 ? candidates : undefined,368context: batch.context ? {369bufferPath: batch.context.bufferPath,370totalEntries: batch.context.totalEntries,371totalOps: batch.context.totalOps,372chunkIndex: batch.context.chunkIndex,373chunkTotal: batch.context.chunkTotal,374totalApplyOps: batch.context.totalApplyOps,375} : undefined,376};377}378379export function compactManualApplyCandidates(candidates, cwd = process.cwd()) {380return (Array.isArray(candidates) ? candidates : [])381.slice(0, 24)382.map((candidate) => ({383entryId: candidate.entryId,384ref: candidate.ref,385sourceHint: compactManualApplySourceMatch(candidate.sourceHint, cwd),386textMatches: compactManualApplySourceMatches(candidate.textMatches, 8, cwd),387objectKeyMatches: compactManualApplySourceMatches(candidate.objectKeyMatches, 8, cwd),388contextTextMatches: compactManualApplySourceMatches(candidate.contextTextMatches, 8, cwd),389locatorMatches: compactManualApplySourceMatches(candidate.locatorMatches, 6, cwd),390}));391}392393function compactManualApplySourceMatches(matches, limit, cwd) {394return (Array.isArray(matches) ? matches : [])395.slice(0, limit)396.map((match) => compactManualApplySourceMatch(match, cwd))397.filter(Boolean);398}399400function compactManualApplySourceMatch(match, cwd) {401if (!match || typeof match !== 'object') return null;402const file = match.relativeFile || match.file;403if (!file && !match.line) return null;404return {405file: summarizeManualLogFile(file, cwd),406line: match.line || null,407column: match.column || null,408reason: match.reason || match.kind || undefined,409status: match.status || undefined,410};411}412413function compactManualApplyEntry(entry = {}) {414return {415id: entry.id,416pageUrl: entry.pageUrl,417stagedAt: entry.stagedAt || null,418element: compactManualApplyContext(entry.element),419ops: (entry.ops || []).map(compactManualApplyOp),420};421}422423function compactManualApplyOp(op = {}) {424return {425entryId: op.entryId,426ref: op.ref,427contextRef: op.contextRef,428tag: op.tag,429elementId: op.elementId,430classes: Array.isArray(op.classes) ? op.classes : [],431originalText: op.originalText,432newText: op.newText,433deleted: op.deleted === true || undefined,434sourceHint: op.sourceHint || null,435leaf: compactManualApplyContext(op.leaf),436nearbyEditableTexts: compactNearbyManualEditTexts(op.nearbyEditableTexts),437container: compactManualApplyContext(op.container),438contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 8) : undefined,439};440}441442function compactManualApplyContext(value) {443if (!value || typeof value !== 'object') return null;444return {445ref: value.ref,446tagName: value.tagName || value.tag || null,447id: value.id || null,448classes: Array.isArray(value.classes) ? value.classes : [],449textContent: truncateManualApplyText(value.textContent, MANUAL_APPLY_COMPACT_TEXT_LIMIT),450};451}452453function compactNearbyManualEditTexts(items) {454return (Array.isArray(items) ? items : [])455.slice(0, MANUAL_APPLY_COMPACT_NEARBY_LIMIT)456.map((item) => typeof item === 'string' ? { text: truncateManualApplyText(item, MANUAL_APPLY_COMPACT_TEXT_LIMIT) } : {457ref: item?.ref,458tag: item?.tag,459classes: Array.isArray(item?.classes) ? item.classes : [],460text: truncateManualApplyText(item?.text, MANUAL_APPLY_COMPACT_TEXT_LIMIT),461});462}463464function truncateManualApplyText(value, max) {465if (typeof value !== 'string') return value || null;466return value.length > max ? value.slice(0, max) : value;467}468469function normalizeApplyChunkResult(result) {470const status = result?.status === 'partial' ? 'partial' : result?.status === 'error' ? 'error' : 'done';471return {472status,473message: typeof result?.message === 'string' ? result.message : null,474appliedEntryIds: Array.isArray(result?.appliedEntryIds) ? result.appliedEntryIds.filter((id) => typeof id === 'string') : [],475failed: Array.isArray(result?.failed) ? result.failed.filter(Boolean) : [],476files: Array.isArray(result?.files) ? result.files.filter((file) => typeof file === 'string') : [],477notes: Array.isArray(result?.notes) ? result.notes.filter((note) => typeof note === 'string') : [],478};479}480481function manualApplyResultShapeHint(eventId = 'EVENT_ID') {482return `Use live-poll.mjs --reply ${eventId} done --data '{"status":"done","appliedEntryIds":["ENTRY_ID"],"failed":[],"files":["src/page.html"],"notes":[]}'`;483}484485function invalidManualApplyResult(reason, eventId, extra = {}) {486return {487ok: false,488body: {489error: 'invalid_manual_apply_result',490reason,491hint: manualApplyResultShapeHint(eventId),492...extra,493},494};495}496497export function validateManualApplyResultMessage(msg, deferred) {498let data = msg?.data;499const eventId = msg?.id || deferred?.event?.id || 'EVENT_ID';500if (!data || typeof data !== 'object' || Array.isArray(data)) {501return invalidManualApplyResult('missing_result_data', eventId);502}503if ('entries' in data || 'ops' in data) {504return invalidManualApplyResult('summary_result_not_allowed', eventId);505}506if (!['done', 'partial', 'error'].includes(data.status)) {507return invalidManualApplyResult('invalid_status', eventId, { status: data.status ?? null });508}509510for (const key of ['appliedEntryIds', 'failed', 'files', 'notes']) {511if (!Array.isArray(data[key])) {512return invalidManualApplyResult(`${key}_must_be_array`, eventId);513}514}515516for (const [index, value] of data.appliedEntryIds.entries()) {517if (typeof value !== 'string' || !value) {518return invalidManualApplyResult('appliedEntryIds_must_contain_strings', eventId, { index });519}520}521for (const [index, value] of data.files.entries()) {522if (typeof value !== 'string' || !value) {523return invalidManualApplyResult('files_must_contain_strings', eventId, { index });524}525}526for (const [index, value] of data.notes.entries()) {527if (typeof value !== 'string') {528return invalidManualApplyResult('notes_must_contain_strings', eventId, { index });529}530}531for (const [index, item] of data.failed.entries()) {532if (!item || typeof item !== 'object' || Array.isArray(item)) {533return invalidManualApplyResult('failed_must_contain_objects', eventId, { index });534}535if (typeof item.entryId !== 'string' || !item.entryId) {536return invalidManualApplyResult('failed_entryId_required', eventId, { index });537}538if (typeof item.reason !== 'string' || !item.reason) {539return invalidManualApplyResult('failed_reason_required', eventId, { index });540}541}542543const eventEntryIds = new Set((deferred?.batch?.entries || []).map((entry) => entry.id).filter(Boolean));544for (const entryId of data.appliedEntryIds) {545if (eventEntryIds.size > 0 && !eventEntryIds.has(entryId)) {546return invalidManualApplyResult('applied_entry_id_not_in_event', eventId, { entryId });547}548}549for (const item of data.failed) {550if (eventEntryIds.size > 0 && !eventEntryIds.has(item.entryId)) {551return invalidManualApplyResult('failed_entry_id_not_in_event', eventId, { entryId: item.entryId });552}553}554555if (data.status === 'done') {556if (data.failed.length > 0) {557return invalidManualApplyResult('done_result_has_failed_entries', eventId);558}559if (countManualApplyOps(deferred?.batch) > 0 && data.appliedEntryIds.length === 0) {560return invalidManualApplyResult('done_result_missing_applied_entry_ids', eventId);561}562}563if (data.status === 'partial' && data.appliedEntryIds.length === 0 && data.failed.length === 0) {564return invalidManualApplyResult('partial_result_has_no_entries', eventId);565}566if (data.status === 'error' && data.appliedEntryIds.length > 0) {567return invalidManualApplyResult('error_result_has_applied_entries', eventId);568}569570return {571ok: true,572result: {573status: data.status,574message: typeof data.message === 'string' ? data.message : undefined,575appliedEntryIds: data.appliedEntryIds,576failed: data.failed,577files: data.files,578notes: data.notes,579},580};581}582583function firstFailureReason(result) {584const first = Array.isArray(result?.failed) ? result.failed.find(Boolean) : null;585return first?.reason || first?.message || null;586}587588function markChunkEntriesFailed(failedByEntry, chunk, reason) {589for (const entryId of chunk.entryIds) {590if (failedByEntry.has(entryId)) continue;591failedByEntry.set(entryId, { entryId, reason, candidates: [] });592}593}594595export function splitManualApplyBatch(batch, maxOps) {596const totalOpCount = countManualApplyOps(batch);597if (totalOpCount <= maxOps) {598return [{599batch,600meta: null,601entryIds: new Set((batch?.entries || []).map((entry) => entry.id).filter(Boolean)),602opCountsByEntry: new Map((batch?.entries || []).map((entry) => [entry.id, Array.isArray(entry.ops) ? entry.ops.length : 0])),603}];604}605606const rawChunks = [];607let current = createManualApplyChunkBuilder();608for (const entry of batch?.entries || []) {609const ops = entry.ops || [];610if (ops.length <= maxOps) {611if (current.opCount > 0 && current.opCount + ops.length > maxOps) {612rawChunks.push(current);613current = createManualApplyChunkBuilder();614}615for (const op of ops) addOpToManualApplyChunk(current, entry, op);616continue;617}618if (current.opCount > 0) {619rawChunks.push(current);620current = createManualApplyChunkBuilder();621}622for (const op of ops) {623if (current.opCount >= maxOps) {624rawChunks.push(current);625current = createManualApplyChunkBuilder();626}627addOpToManualApplyChunk(current, entry, op);628}629}630if (current.opCount > 0) rawChunks.push(current);631632return rawChunks.map((chunk, index) => ({633batch: {634...batch,635count: chunk.opCount,636entries: chunk.entries,637ops: chunk.ops,638candidates: filterManualApplyChunkCandidates(batch, chunk.refsByEntry),639context: {640...(batch?.context || {}),641totalEntries: chunk.entries.length,642totalOps: chunk.opCount,643chunkIndex: index + 1,644chunkTotal: rawChunks.length,645totalApplyOps: totalOpCount,646},647},648meta: {649index: index + 1,650total: rawChunks.length,651opCount: chunk.opCount,652totalOpCount,653},654entryIds: new Set(chunk.entries.map((entry) => entry.id).filter(Boolean)),655opCountsByEntry: chunk.opCountsByEntry,656}));657}658659function createManualApplyChunkBuilder() {660return {661entries: [],662entryById: new Map(),663entryIds: new Set(),664ops: [],665refsByEntry: new Map(),666opCountsByEntry: new Map(),667opCount: 0,668};669}670671function addOpToManualApplyChunk(chunk, entry, op) {672let chunkEntry = chunk.entryById.get(entry.id);673if (!chunkEntry) {674chunkEntry = { ...entry, ops: [] };675chunk.entryById.set(entry.id, chunkEntry);676chunk.entryIds.add(entry.id);677chunk.entries.push(chunkEntry);678}679chunkEntry.ops.push(op);680chunk.ops.push({ ...op, entryId: op.entryId || entry.id });681if (!chunk.refsByEntry.has(entry.id)) chunk.refsByEntry.set(entry.id, new Set());682if (op.ref) chunk.refsByEntry.get(entry.id).add(op.ref);683chunk.opCountsByEntry.set(entry.id, (chunk.opCountsByEntry.get(entry.id) || 0) + 1);684chunk.opCount += 1;685}686687function filterManualApplyChunkCandidates(batch, refsByEntry) {688return (batch?.candidates || []).filter((candidate) => {689const refs = refsByEntry.get(candidate.entryId);690if (!refs) return false;691if (!candidate.ref) return true;692return refs.has(candidate.ref);693});694}695696export function snapshotApplyEventFiles(batch, cwd = process.cwd()) {697const snapshot = new Map();698for (const relativeFile of collectManualApplyFiles(batch, [], cwd)) {699const absolute = path.resolve(cwd, relativeFile);700try {701snapshot.set(relativeFile, {702exists: fs.existsSync(absolute),703content: fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf-8') : '',704});705} catch {706// If a file cannot be read before dispatch, do not attempt late rollback.707}708}709return snapshot;710}711712export function manualApplyTransactionPath(cwd = process.cwd()) {713return path.join(getLiveDir(cwd), 'manual-edit-apply-transaction.json');714}715716export function readManualApplyTransaction(cwd = process.cwd()) {717const file = manualApplyTransactionPath(cwd);718if (!fs.existsSync(file)) return null;719try {720return JSON.parse(fs.readFileSync(file, 'utf-8'));721} catch {722return null;723}724}725726export function writeManualApplyTransaction({ cwd = process.cwd(), pageUrl = null, batch }) {727const file = manualApplyTransactionPath(cwd);728const files = collectManualApplyFiles(batch, [], cwd);729const transaction = {730version: 1,731id: randomUUID().replace(/-/g, '').slice(0, 8),732createdAt: new Date().toISOString(),733pageUrl,734entryIds: (batch?.entries || []).map((entry) => entry.id).filter(Boolean),735files: files.map((relativeFile) => {736const absolute = path.resolve(cwd, relativeFile);737const exists = fs.existsSync(absolute);738return {739file: relativeFile,740exists,741content: exists ? fs.readFileSync(absolute, 'utf-8') : '',742};743}),744};745fs.mkdirSync(path.dirname(file), { recursive: true });746fs.writeFileSync(`${file}.tmp`, JSON.stringify(transaction, null, 2) + '\n', 'utf-8');747fs.renameSync(`${file}.tmp`, file);748return transaction;749}750751export function clearManualApplyTransaction(cwd = process.cwd(), transactionId = null) {752const file = manualApplyTransactionPath(cwd);753if (!fs.existsSync(file)) return false;754if (transactionId) {755const existing = readManualApplyTransaction(cwd);756if (existing?.id && existing.id !== transactionId) return false;757}758try {759fs.unlinkSync(file);760return true;761} catch {762return false;763}764}765766export function rollbackManualApplyTransaction({767cwd = process.cwd(),768pageUrl = null,769reason = 'manual_edit_transaction_rollback',770recordManualEditActivity = null,771} = {}) {772const transaction = readManualApplyTransaction(cwd);773if (!transaction) return null;774if (pageUrl && transaction.pageUrl && transaction.pageUrl !== pageUrl) return null;775776let pendingIds = new Set();777try {778const buffer = readManualEditsBuffer(cwd);779pendingIds = new Set((buffer.entries || []).map((entry) => entry.id).filter(Boolean));780} catch {781pendingIds = new Set(transaction.entryIds || []);782}783const shouldRollback = (transaction.entryIds || []).some((id) => pendingIds.has(id));784if (!shouldRollback) {785clearManualApplyTransaction(cwd, transaction.id);786return { id: transaction.id, reason, rolledBackFiles: [], rollbackFailures: [], skipped: 'entries_not_pending' };787}788789const rolledBackFiles = [];790const rollbackFailures = [];791for (const item of transaction.files || []) {792const relativeFile = normalizeProjectFile(item.file, cwd);793if (!relativeFile) continue;794const absolute = path.resolve(cwd, relativeFile);795try {796if (item.exists) {797fs.mkdirSync(path.dirname(absolute), { recursive: true });798fs.writeFileSync(absolute, item.content || '', 'utf-8');799} else if (fs.existsSync(absolute)) {800fs.rmSync(absolute);801}802rolledBackFiles.push(relativeFile);803} catch (err) {804rollbackFailures.push({ file: relativeFile, reason: 'restore_failed', message: err.message || String(err) });805}806}807clearManualApplyTransaction(cwd, transaction.id);808recordManualEditActivity?.('manual_edit_transaction_rolled_back', {809id: transaction.id,810pageUrl: transaction.pageUrl || null,811reason,812entryIds: transaction.entryIds || [],813rolledBackFiles: rolledBackFiles.map((file) => summarizeManualLogFile(file, cwd)).filter(Boolean),814rollbackFailures: summarizeManualDiagnostics(rollbackFailures, cwd),815});816return { id: transaction.id, reason, rolledBackFiles, rollbackFailures };817}818819export function collectManualApplyFiles(batch, extraFiles = [], cwd = process.cwd()) {820const files = [];821for (const entry of batch?.entries || []) {822for (const op of entry.ops || []) files.push(op.sourceHint?.file);823}824for (const candidate of batch?.candidates || []) {825files.push(candidate.sourceHint?.relativeFile, candidate.sourceHint?.file);826for (const item of candidate.textMatches || []) files.push(item.file);827for (const item of candidate.objectKeyMatches || []) files.push(item.file);828for (const item of candidate.locatorMatches || []) files.push(item.file);829for (const item of candidate.contextTextMatches || []) files.push(item.file);830}831files.push(...(extraFiles || []));832return [...new Set(files)]833.map((file) => normalizeProjectFile(file, cwd))834.filter(Boolean);835}836837function normalizeProjectFile(file, cwd = process.cwd()) {838if (!file || typeof file !== 'string') return null;839const absolute = path.isAbsolute(file) ? file : path.resolve(cwd, file);840const relative = path.relative(cwd, absolute);841if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;842return relative;843}844845export function rollbackApplySnapshot(846batch,847rollbackSnapshot,848extraFiles = [],849_reason = 'manual_edit_apply_snapshot_rollback',850cwd = process.cwd(),851) {852const scope = collectManualApplyFiles(batch, extraFiles, cwd);853const rolledBackFiles = [];854const rollbackFailures = [];855for (const relativeFile of scope) {856const before = rollbackSnapshot?.get(relativeFile);857if (!before) continue;858const absolute = path.resolve(cwd, relativeFile);859try {860if (before.exists) {861fs.mkdirSync(path.dirname(absolute), { recursive: true });862fs.writeFileSync(absolute, before.content, 'utf-8');863} else if (fs.existsSync(absolute)) {864fs.rmSync(absolute);865}866rolledBackFiles.push(relativeFile);867} catch (err) {868rollbackFailures.push({ file: relativeFile, reason: 'restore_failed', message: err.message || String(err) });869}870}871return { rolledBackFiles, rollbackFailures };872}873874function manualApplyReplyCommand(eventOrId = 'EVENT_ID') {875const id = typeof eventOrId === 'string' ? eventOrId : eventOrId?.id || 'EVENT_ID';876return `live-poll.mjs --reply ${id} done --data '<json>'`;877}878879export function buildManualApplyAgentAction(eventOrId = 'EVENT_ID') {880return {881kind: 'manual_edit_apply',882required: 'apply_source_edits_then_reply',883replyCommand: manualApplyReplyCommand(eventOrId),884warning: 'Polling only leases this work item; it does not commit source edits.',885};886}887888export function summarizeManualApplyEvent(event = {}, batch = event.batch, cwd = process.cwd()) {889const entries = Array.isArray(batch?.entries) ? batch.entries : [];890const opCount = entries.reduce((sum, entry) => sum + (Array.isArray(entry.ops) ? entry.ops.length : 0), 0);891return {892pageUrl: event.pageUrl || null,893chunk: event.chunk || null,894entryCount: entries.length,895opCount,896files: collectManualApplyFiles(batch, [], cwd),897};898}899900export function summarizeManualApplyFailures(failed, cwd = process.cwd()) {901if (!Array.isArray(failed)) return [];902return failed.slice(0, 20).map((item) => ({903id: item.id || item.entryId || null,904reason: item.reason || item.message || 'failed',905message: compactManualLogText(item.message, 300),906files: Array.isArray(item.files) ? item.files.slice(0, 12).map((file) => summarizeManualLogFile(file, cwd)).filter(Boolean) : undefined,907checks: summarizeManualDiagnostics(item.checks, cwd),908failures: summarizeManualDiagnostics(item.failures, cwd),909candidates: summarizeManualDiagnostics(item.candidates, cwd),910}));911}912913export function summarizeManualDiagnostics(items, cwd = process.cwd()) {914if (!Array.isArray(items) || items.length === 0) return undefined;915return items.slice(0, 12).map((item) => ({916reason: item.reason || item.kind || undefined,917detail: compactManualLogText(item.detail, 220),918message: compactManualLogText(item.message, 300),919file: summarizeManualLogFile(item.file || item.relativeFile, cwd),920line: item.line || undefined,921ref: compactManualLogText(item.ref, 180),922marker: compactManualLogText(item.marker, 120),923files: Array.isArray(item.files) ? item.files.slice(0, 8).map((file) => summarizeManualLogFile(file, cwd)).filter(Boolean) : undefined,924}));925}926927export function summarizeManualLogFile(file, cwd = process.cwd()) {928if (!file || typeof file !== 'string') return undefined;929if (!path.isAbsolute(file)) return file;930const relative = path.relative(cwd, file);931return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;932}933934export function compactManualLogText(value, max = 200) {935if (typeof value !== 'string') return undefined;936const normalized = value.replace(/\s+/g, ' ').trim();937if (normalized.length <= max) return normalized;938return normalized.slice(0, max) + `... [truncated ${normalized.length - max} chars]`;939}940