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-commit-manual-edits.mjs
1#!/usr/bin/env node2/**3* CLI helper: apply pending live copy edits as one AI-owned batch.4*5* The browser Save path stages copy edits in .impeccable/live. This script is6* called by /manual-edit-commit when the user clicks Apply copy edits. It gives7* the local AI runner the full staged batch plus evidence, validates the files8* the runner reports touching, and clears only entries reported as applied.9*10* Usage:11* node live-commit-manual-edits.mjs12* node live-commit-manual-edits.mjs --page-url=/13*14* Output JSON:15* { applied, failed, files, cleared, count, pageUrl }16*/1718import { buildManualEditEvidence } from './live-manual-edit-evidence.mjs';19import { readBuffer, readBufferStrict, writeBuffer, countByPage } from './live/manual-edits-buffer.mjs';20import { isGeneratedFile } from './lib/is-generated.mjs';21import {22runCopyEditBatchAgent,23runCopyEditPostApplyChecks,24} from './live-copy-edit-agent.mjs';25import fs from 'node:fs';26import path from 'node:path';2728const ROLLBACK_EXTENSIONS = new Set([29'.astro',30'.cjs',31'.css',32'.htm',33'.html',34'.js',35'.json',36'.jsx',37'.md',38'.mdx',39'.mjs',40'.scss',41'.svelte',42'.svg',43'.ts',44'.tsx',45'.txt',46'.vue',47'.yaml',48'.yml',49]);50const ROLLBACK_SKIP_DIRS = new Set([51'.astro',52'.git',53'.impeccable',54'.next',55'.nuxt',56'.svelte-kit',57'build',58'coverage',59'dist',60'node_modules',61'out',62]);63const DEFAULT_REPAIR_ATTEMPTS = 3;6465function argVal(args, name) {66const prefix = name + '=';67for (const arg of args) {68if (arg === name) return true;69if (arg.startsWith(prefix)) return arg.slice(prefix.length);70}71return null;72}7374function countOps(entries) {75let count = 0;76for (const entry of entries || []) count += Array.isArray(entry.ops) ? entry.ops.length : 0;77return count;78}7980function summarizeAppliedEntries(entries, appliedEntryIds) {81const ids = new Set(appliedEntryIds);82const out = [];83for (const entry of entries || []) {84if (!ids.has(entry.id)) continue;85for (const op of entry.ops || []) {86out.push({87id: entry.id,88ref: op.ref,89originalText: op.originalText,90newText: op.newText,91});92}93}94return out;95}9697function normalizeFailedEntries(batch, result, fallbackReason) {98const failed = [];99const failedByEntryId = new Map();100for (const item of result?.failed || []) {101const entryId = item.entryId || item.id || null;102if (!entryId) continue;103failedByEntryId.set(entryId, item);104}105106for (const entry of batch.entries || []) {107const item = failedByEntryId.get(entry.id);108if (!item) continue;109failed.push({110id: entry.id,111reason: item.reason || item.message || fallbackReason || 'failed',112candidates: Array.isArray(item.candidates) && item.candidates.length > 0113? item.candidates114: candidatesForEntry(batch, entry.id),115});116}117return failed;118}119120function mergeFailedEntries(...groups) {121const out = [];122const indexById = new Map();123for (const item of groups.flatMap((group) => Array.isArray(group) ? group : [])) {124if (!item || typeof item !== 'object') continue;125const id = typeof item.id === 'string' && item.id ? item.id : null;126if (!id) {127out.push(item);128continue;129}130const existingIndex = indexById.get(id);131if (existingIndex === undefined) {132indexById.set(id, out.length);133out.push(item);134continue;135}136out[existingIndex] = {137...out[existingIndex],138...item,139candidates: item.candidates || out[existingIndex].candidates,140checks: item.checks || out[existingIndex].checks,141};142}143return out;144}145146function candidatesForEntry(batch, entryId) {147return (batch.candidates || [])148.filter((candidate) => candidate.entryId === entryId)149.flatMap((candidate) => [150...(candidate.sourceHint ? [candidate.sourceHint] : []),151...(candidate.textMatches || []),152...(candidate.objectKeyMatches || []),153...(candidate.locatorMatches || []),154...(candidate.contextTextMatches || []),155])156.slice(0, 12);157}158159function uniqueStrings(values) {160return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))];161}162163function allEntryIds(batch) {164return (batch?.entries || []).map((entry) => entry.id).filter(Boolean);165}166167function mergeUniqueStrings(...groups) {168return uniqueStrings(groups.flatMap((group) => Array.isArray(group) ? group : []));169}170171function repairAttemptLimit(env = process.env) {172const value = Number(env.IMPECCABLE_LIVE_MANUAL_EDIT_REPAIR_ATTEMPTS || DEFAULT_REPAIR_ATTEMPTS);173if (!Number.isFinite(value)) return DEFAULT_REPAIR_ATTEMPTS;174return Math.max(1, Math.min(10, Math.trunc(value)));175}176177function summarizeRepairFailures(failures = []) {178return failures.map((failure) => {179const out = {180reason: failure.reason || failure.detail || 'validation_failed',181};182if (failure.id || failure.entryId) out.entryId = failure.id || failure.entryId;183if (failure.ref) out.ref = failure.ref;184if (failure.detail) out.detail = failure.detail;185if (failure.file) out.file = failure.file;186if (failure.message) out.message = failure.message;187if (failure.marker) out.marker = failure.marker;188if (Array.isArray(failure.files)) out.files = failure.files.slice(0, 8);189if (Array.isArray(failure.candidates)) {190out.candidates = failure.candidates.slice(0, 8).map((candidate) => ({191file: candidate.file,192line: candidate.line,193kind: candidate.kind,194reason: candidate.reason,195}));196}197if (Array.isArray(failure.failures)) {198out.failures = failure.failures.slice(0, 8).map((item) => ({199ref: item.ref,200reason: item.reason || item.detail,201detail: item.detail,202candidates: Array.isArray(item.candidates)203? item.candidates.slice(0, 6).map((candidate) => ({204file: candidate.file,205line: candidate.line,206kind: candidate.kind,207reason: candidate.reason,208}))209: undefined,210}));211}212if (failure.checks) out.checks = failure.checks;213return out;214}).slice(0, 20);215}216217function buildRepairBatch(batch, repair) {218return {219...batch,220repair,221};222}223224function normalizeProjectSourcePath(cwd, file, opts = {}) {225if (!file || typeof file !== 'string') return null;226const absolute = path.isAbsolute(file) ? file : path.resolve(cwd, file);227const relative = path.relative(cwd, absolute);228if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;229if (opts.requireExists && !fs.existsSync(absolute)) return null;230if (isGeneratedFile(absolute, { cwd })) return null;231return relative;232}233234function normalizeRelativeFile(cwd, file) {235return normalizeProjectSourcePath(cwd, file, { requireExists: true });236}237238function sourceHintWindowFailure(cwd, op) {239const hint = op?.sourceHint;240if (!hint?.file || !hint.line) return null;241const relative = normalizeRelativeFile(cwd, hint.file);242if (!relative) return null;243const absolute = path.resolve(cwd, relative);244let content;245try { content = fs.readFileSync(absolute, 'utf-8'); } catch { return null; }246const lines = content.split('\n');247const line = Math.max(1, Number(hint.line) || 1);248const lineText = lines[line - 1] || '';249const start = Math.max(0, line - 5);250const end = Math.min(lines.length, line + 4);251if (252typeof op.originalText === 'string'253&& op.originalText254&& lineText.includes(op.originalText)255&& !lineShowsAppliedOp(lineText, op)256) {257return {258file: relative,259line,260reason: 'source_hint_still_contains_original_text',261};262}263if (lines.slice(start, end).some((candidateLine) => lineShowsAppliedOp(candidateLine, op))) return null;264return null;265}266267function verificationTargetsForOp(batch, op, reportedFiles, cwd) {268const candidate = (batch.candidates || []).find((item) => item.entryId === op.entryId && item.ref === op.ref);269const out = [];270const reportedFileSet = new Set(reportedFiles || []);271const add = (file, line, kind) => {272const relativeFile = normalizeRelativeFile(cwd, file);273const lineNumber = Number(line);274if (!relativeFile || !Number.isFinite(lineNumber) || lineNumber < 1) return;275out.push({ file: relativeFile, line: lineNumber, kind, reported: reportedFileSet.has(relativeFile) });276};277278add(op.sourceHint?.file, op.sourceHint?.line, 'source_hint');279add(candidate?.sourceHint?.relativeFile || candidate?.sourceHint?.file, candidate?.sourceHint?.line, 'candidate_source_hint');280for (const item of candidate?.textMatches || []) add(item.file, item.line, 'text_match');281for (const item of candidate?.objectKeyMatches || []) add(item.file, item.line, 'object_key_match');282for (const item of candidate?.locatorMatches || []) add(item.file, item.line, 'locator_match');283for (const item of candidate?.contextTextMatches || []) add(item.file, item.line, 'context_text_match');284285// Manual copy edits often stage coupled leaves from the same UI object, e.g.286// a card label plus its count. Dynamic source stores both on the label/key287// line, so the count op may need the sibling label's data candidates.288for (const siblingCandidate of siblingCandidatesForEntry(batch, op)) {289add(siblingCandidate.sourceHint?.relativeFile || siblingCandidate.sourceHint?.file, siblingCandidate.sourceHint?.line, 'entry_source_hint');290for (const item of siblingCandidate.textMatches || []) add(item.file, item.line, 'entry_text_match');291for (const item of siblingCandidate.objectKeyMatches || []) add(item.file, item.line, 'entry_object_key_match');292for (const item of siblingCandidate.contextTextMatches || []) add(item.file, item.line, 'entry_context_text_match');293}294295for (const relativeFile of reportedFiles || []) {296for (const target of locatorTargetsInFile(cwd, relativeFile, op)) {297out.push(target);298}299}300301const seen = new Set();302return out.filter((target) => {303const key = target.file + ':' + target.line + ':' + target.kind;304if (seen.has(key)) return false;305seen.add(key);306return true;307});308}309310function objectKeyCandidatesForOp(batch, op) {311const candidates = (batch.candidates || [])312.filter((item) => item.entryId === op.entryId && item.ref === op.ref);313return candidates.flatMap((candidate) => candidate.objectKeyMatches || []);314}315316function lineHasObjectKey(line, text) {317if (typeof text !== 'string' || text.length === 0) return false;318const quotedKey = new RegExp('(^|[\\s,{])([\'"`])' + escapeRegExp(text) + '\\2\\s*:');319if (quotedKey.test(line)) return true;320const identifierSafe = /^[A-Za-z_$][\w$]*$/.test(text);321if (!identifierSafe) return false;322const bareKey = new RegExp('(^|[\\s,{])' + escapeRegExp(text) + '\\s*:');323return bareKey.test(line);324}325326function objectKeyMatchStillUsesOriginal(cwd, match, op) {327const relative = normalizeRelativeFile(cwd, match?.file);328const lineNumber = Number(match?.line);329if (!relative || !Number.isFinite(lineNumber) || lineNumber < 1) return false;330let lines;331try { lines = fs.readFileSync(path.resolve(cwd, relative), 'utf-8').split('\n'); } catch { return false; }332const start = Math.max(0, lineNumber - 4);333const end = Math.min(lines.length, lineNumber + 3);334const windowLines = lines.slice(start, end);335if (windowLines.some((line) => lineHasObjectKey(line, op.newText))) return false;336return windowLines.some((line) => lineHasObjectKey(line, op.originalText));337}338339function coupledObjectKeyFailuresForOp(batch, op, cwd) {340if (341typeof op?.originalText !== 'string'342|| typeof op?.newText !== 'string'343|| op.originalText === op.newText344) return [];345return objectKeyCandidatesForOp(batch, op)346.filter((match) => objectKeyMatchStillUsesOriginal(cwd, match, op))347.map((match) => ({348ref: op.ref,349reason: 'source_verification_failed',350detail: 'edited_text_source_key_dependency_not_updated',351candidates: [{352file: normalizeRelativeFile(cwd, match.file) || match.file,353line: match.line,354kind: 'object_key_match',355reason: 'edited text is also a source key; update the coupled key to newText or fail the entry',356}],357}));358}359360function siblingCandidatesForEntry(batch, op) {361if (!op?.entryId) return [];362return (batch.candidates || []).filter((item) => item.entryId === op.entryId && item.ref !== op.ref);363}364365function locatorTargetsInFile(cwd, relativeFile, op) {366if (!opHasLocator(op)) return [];367const absolute = path.resolve(cwd, relativeFile);368let lines;369try { lines = fs.readFileSync(absolute, 'utf-8').split('\n'); } catch { return []; }370const out = [];371for (let index = 0; index < lines.length; index += 1) {372if (!lineMatchesManualEditLocator(lines[index], op)) continue;373out.push({ file: relativeFile, line: index + 1, kind: 'reported_locator_match' });374if (out.length >= 20) break;375}376return out;377}378379function verificationTargetPasses(cwd, target, op) {380let lines;381try { lines = fs.readFileSync(path.resolve(cwd, target.file), 'utf-8').split('\n'); } catch { return false; }382return verificationTargetPassesLines(lines, target, op);383}384385function verificationTargetPassesLines(lines, target, op) {386const line = lines[target.line - 1] || '';387if (lineShowsAppliedOp(line, op)) return true;388const originalText = typeof op?.originalText === 'string' ? op.originalText : '';389if (originalText && line.includes(originalText)) return false;390const kind = String(target.kind || '');391const canSearchWindow = target.reported392|| kind.includes('context_text_match')393|| kind.includes('object_key_match')394|| kind.includes('text_match');395if (!canSearchWindow) return false;396const radius = kind.includes('context_text_match') ? 20 : 4;397const start = Math.max(0, target.line - radius - 1);398const end = Math.min(lines.length, target.line + radius);399const windowLines = lines.slice(start, end);400if (windowLines.some((candidateLine) => lineShowsAppliedOp(candidateLine, op))) return true;401if (windowShowsAppliedOp(windowLines, op)) return true;402return false;403}404405function windowShowsAppliedOp(lines, op) {406const newText = typeof op?.newText === 'string' ? op.newText : '';407if (!newText) return false;408const originalText = typeof op?.originalText === 'string' ? op.originalText : '';409const normalizedNew = normalizeVerificationText(newText);410const normalizedOriginal = normalizeVerificationText(originalText);411const normalizedWindow = normalizeVerificationText(lines.join('\n'));412if (!normalizedNew || !normalizedWindow.includes(normalizedNew)) return false;413if (normalizedOriginal && !normalizedNew.includes(normalizedOriginal) && normalizedWindow.includes(normalizedOriginal)) return false;414return true;415}416417function normalizeVerificationText(text) {418return String(text || '').replace(/\s+/g, ' ').trim();419}420421function lineShowsAppliedOp(line, op) {422const originalText = typeof op?.originalText === 'string' ? op.originalText : '';423const newText = typeof op?.newText === 'string' ? op.newText : '';424const deletion = op?.deleted === true || newText.length === 0;425if (deletion) return !!originalText && !line.includes(originalText);426if (!line.includes(newText)) return false;427if (originalText && !newText.includes(originalText) && line.includes(originalText)) return false;428return true;429}430431function opHasLocator(op) {432return !!(433op?.tag434|| op?.elementId435|| (Array.isArray(op?.classes) && op.classes.filter(Boolean).length > 0)436);437}438439function lineMatchesManualEditLocator(line, op) {440if (op.tag) {441const tagRe = new RegExp('<\\s*' + escapeRegExp(op.tag) + '(?=[\\s>/]|$)', 'i');442if (!tagRe.test(line)) return false;443}444445if (op.elementId) {446const idRe = new RegExp('\\bid\\s*=\\s*["\']' + escapeRegExp(op.elementId) + '["\']');447if (!idRe.test(line)) return false;448}449450const classes = Array.isArray(op.classes) ? op.classes.filter(Boolean) : [];451for (const className of classes) {452if (!line.includes(className)) return false;453}454455return true;456}457458function verifyAppliedEntry({ batch, entry, reportedFiles, cwd }) {459const failures = [];460for (const rawOp of entry.ops || []) {461const op = { ...rawOp, entryId: entry.id };462if (op.deleted === true && typeof op.newText !== 'string') op.newText = '';463if (typeof op.newText !== 'string') {464failures.push({465ref: op.ref,466reason: 'source_verification_failed',467detail: 'missing_newText',468candidates: candidatesForEntry(batch, entry.id).slice(0, 12),469});470continue;471}472const targets = verificationTargetsForOp(batch, op, reportedFiles, cwd);473const coupledObjectKeyFailures = coupledObjectKeyFailuresForOp(batch, op, cwd);474if (475coupledObjectKeyFailures.length === 0476&& targets.some((target) => verificationTargetPasses(cwd, target, op))477) continue;478479if (coupledObjectKeyFailures.length > 0) {480failures.push(...coupledObjectKeyFailures.map((failure) => ({481...failure,482candidates: [483...(failure.candidates || []),484...targets.map((target) => ({ file: target.file, line: target.line, kind: target.kind })),485...candidatesForEntry(batch, entry.id),486].slice(0, 12),487})));488continue;489}490491const hintedOldText = sourceHintWindowFailure(cwd, op);492if (hintedOldText) {493failures.push({494ref: op.ref,495reason: 'source_verification_failed',496detail: hintedOldText.reason,497candidates: [hintedOldText, ...targets.map((target) => ({ file: target.file, line: target.line, kind: target.kind })), ...candidatesForEntry(batch, entry.id)].slice(0, 12),498});499continue;500}501502failures.push({503ref: op.ref,504reason: 'source_verification_failed',505detail: op.newText.length === 0 ? 'originalText_still_present_in_plausible_source_location' : 'newText_not_found_in_plausible_source_location',506candidates: targets.map((target) => ({ file: target.file, line: target.line, kind: target.kind })).concat(candidatesForEntry(batch, entry.id)).slice(0, 12),507});508}509return failures;510}511512function snapshotTargetPasses(snapshot, target, op) {513const before = snapshot.get(target.file)?.content;514if (typeof before !== 'string') return false;515return verificationTargetPassesLines(before.split('\n'), target, op);516}517518function findUnappliedEntrySourceChanges({ batch, entries, reportedFiles, cwd, rollbackSnapshot }) {519const failures = [];520for (const entry of entries || []) {521for (const rawOp of entry.ops || []) {522const op = { ...rawOp, entryId: entry.id };523if (typeof op.newText !== 'string' || op.newText.length === 0) continue;524const targets = verificationTargetsForOp(batch, op, reportedFiles, cwd);525const leakedTargets = targets.filter((target) =>526verificationTargetPasses(cwd, target, op)527&& !snapshotTargetPasses(rollbackSnapshot, target, op)528);529if (leakedTargets.length === 0) continue;530failures.push({531id: entry.id,532reason: 'failed_entry_source_changed',533ref: op.ref,534newText: op.newText,535candidates: leakedTargets536.map((target) => ({ file: target.file, line: target.line, kind: target.kind }))537.concat(candidatesForEntry(batch, entry.id))538.slice(0, 12),539});540break;541}542}543return failures;544}545546function verificationFailuresForEntries(batch, entries, reason, extra = {}) {547return entries.map((entry) => ({548id: entry.id,549reason,550candidates: candidatesForEntry(batch, entry.id),551...extra,552}));553}554555function clearAppliedEntries(cwd, appliedEntryIds) {556const ids = new Set(appliedEntryIds);557if (ids.size === 0) return 0;558const buffer = readBuffer(cwd);559let cleared = 0;560const kept = [];561for (const entry of buffer.entries || []) {562if (ids.has(entry.id)) {563cleared += Array.isArray(entry.ops) ? entry.ops.length : 0;564} else {565kept.push(entry);566}567}568writeBuffer(cwd, { version: buffer.version || 1, entries: kept });569return cleared;570}571572function snapshotRollbackFiles(cwd, files = null) {573const snapshot = new Map();574const rollbackFiles = Array.isArray(files) && files.length > 0575? uniqueStrings(files).map((file) => normalizeRollbackPath(cwd, file)).filter(Boolean)576: collectRollbackFiles(cwd);577for (const relativeFile of rollbackFiles) {578const absolute = path.resolve(cwd, relativeFile);579try {580snapshot.set(relativeFile, {581existed: true,582content: fs.readFileSync(absolute, 'utf-8'),583});584} catch (err) {585if (err?.code === 'ENOENT') {586snapshot.set(relativeFile, { existed: false });587}588// Other read failures are not safe to roll back.589}590}591return snapshot;592}593594function collectRollbackFiles(cwd) {595const out = [];596const seenDirs = new Set();597const seenFiles = new Set();598scanRollbackDir(cwd, cwd, out, seenDirs, seenFiles, 0);599return out;600}601602function scanRollbackDir(dir, cwd, out, seenDirs, seenFiles, depth) {603if (depth > 10) return;604let realDir;605try { realDir = fs.realpathSync(dir); } catch { return; }606if (seenDirs.has(realDir)) return;607seenDirs.add(realDir);608609let entries;610try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }611for (const entry of entries) {612if (entry.isDirectory()) {613if (ROLLBACK_SKIP_DIRS.has(entry.name)) continue;614scanRollbackDir(path.join(dir, entry.name), cwd, out, seenDirs, seenFiles, depth + 1);615continue;616}617if (!entry.isFile()) continue;618if (!ROLLBACK_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) continue;619const absolute = path.join(dir, entry.name);620if (isGeneratedFile(absolute, { cwd })) continue;621let realFile;622try { realFile = fs.realpathSync(absolute); } catch { continue; }623if (seenFiles.has(realFile)) continue;624seenFiles.add(realFile);625const relative = path.relative(cwd, absolute);626if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) continue;627out.push(relative);628}629}630631function changedFilesSinceSnapshot(cwd, snapshot, scopeFiles = null) {632const changed = new Map();633const scopedFiles = Array.isArray(scopeFiles) && scopeFiles.length > 0634? scopeFiles.map((file) => normalizeRollbackPath(cwd, file)).filter(Boolean)635: null;636const currentFiles = new Set(scopedFiles || collectRollbackFiles(cwd));637for (const [relativeFile, before] of snapshot.entries()) {638if (scopedFiles && !currentFiles.has(relativeFile)) continue;639const absolute = path.resolve(cwd, relativeFile);640if (before?.existed === false) {641if (fs.existsSync(absolute)) changed.set(relativeFile, { file: relativeFile, kind: 'added' });642continue;643}644if (!fs.existsSync(absolute)) {645changed.set(relativeFile, { file: relativeFile, kind: 'deleted' });646continue;647}648let content;649try { content = fs.readFileSync(absolute, 'utf-8'); } catch { continue; }650if (content !== before.content) {651changed.set(relativeFile, { file: relativeFile, kind: 'modified' });652}653}654for (const relativeFile of currentFiles) {655if (!snapshot.has(relativeFile)) {656changed.set(relativeFile, { file: relativeFile, kind: 'unknown' });657}658}659return [...changed.values()];660}661662function rollbackChangedFiles(cwd, snapshot, extraFiles = [], scopeFiles = []) {663const scope = new Set(664[...(scopeFiles || []), ...(extraFiles || [])]665.map((file) => normalizeRollbackPath(cwd, file))666.filter(Boolean),667);668const changed = changedFilesSinceSnapshot(cwd, snapshot, [...scope]);669const byFile = new Map(changed.map((item) => [item.file, item]));670for (const file of extraFiles || []) {671const relative = normalizeRollbackPath(cwd, file);672if (relative && !byFile.has(relative)) {673byFile.set(relative, { file: relative, kind: snapshot.has(relative) ? 'reported' : 'unknown' });674}675}676677const rolledBackFiles = [];678const rollbackFailures = [];679for (const item of byFile.values()) {680if (!scope.has(item.file)) continue;681const absolute = path.resolve(cwd, item.file);682const before = snapshot.get(item.file);683try {684if (before?.existed !== false && typeof before?.content === 'string') {685fs.mkdirSync(path.dirname(absolute), { recursive: true });686fs.writeFileSync(absolute, before.content, 'utf-8');687} else if (before?.existed === false && item.kind === 'added' && fs.existsSync(absolute)) {688fs.rmSync(absolute);689} else {690rollbackFailures.push({ file: item.file, reason: 'no_snapshot' });691continue;692}693rolledBackFiles.push(item.file);694} catch (err) {695rollbackFailures.push({ file: item.file, reason: 'restore_failed', message: err.message || String(err) });696}697}698return { rolledBackFiles, rollbackFailures };699}700701function collectApplyOwnedFiles(batch, cwd, extraFiles = []) {702const files = [];703for (const entry of batch?.entries || []) {704for (const op of entry.ops || []) files.push(op.sourceHint?.file);705}706for (const candidate of batch?.candidates || []) {707files.push(candidate.sourceHint?.relativeFile, candidate.sourceHint?.file);708for (const item of candidate.textMatches || []) files.push(item.file);709for (const item of candidate.objectKeyMatches || []) files.push(item.file);710for (const item of candidate.locatorMatches || []) files.push(item.file);711for (const item of candidate.contextTextMatches || []) files.push(item.file);712}713files.push(...(extraFiles || []));714return uniqueStrings(files)715.map((file) => normalizeRollbackPath(cwd, file))716.filter(Boolean);717}718719function unreportedChangedFiles(cwd, snapshot, reportedFiles, scopeFiles = []) {720const reported = new Set(721(reportedFiles || [])722.map((file) => normalizeRollbackPath(cwd, file))723.filter(Boolean),724);725const scope = new Set(726(scopeFiles || [])727.map((file) => normalizeRollbackPath(cwd, file))728.filter(Boolean),729);730return changedFilesSinceSnapshot(cwd, snapshot, [...scope])731.map((item) => item.file)732.filter((file) => scope.has(file))733.filter((file) => !reported.has(file));734}735736function normalizeRollbackPath(cwd, file) {737return normalizeProjectSourcePath(cwd, file);738}739740function verifyEntriesAfterRepair({ batch, appliedEntryIds, files, cwd }) {741const reportedFiles = uniqueStrings(files || [])742.map((file) => normalizeRelativeFile(cwd, file))743.filter(Boolean);744const entries = (batch.entries || []).filter((entry) => appliedEntryIds.includes(entry.id));745const verifiedIds = [];746const failed = [];747for (const entry of entries) {748const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });749if (failures.length === 0) {750verifiedIds.push(entry.id);751} else {752failed.push({753id: entry.id,754reason: 'source_verification_failed',755failures,756candidates: candidatesForEntry(batch, entry.id),757});758}759}760return { verifiedIds, failed, reportedFiles };761}762763async function repairPostApplyValidation({764batch,765cwd,766pageUrl,767count,768provider,769env,770timeoutMs,771applyBatchToSource,772chatAvailable,773transactionId,774appliedEntryIds,775files,776failed,777notes,778warnings,779postChecks,780repairReason = 'post_apply_validation_failed',781repairFailures = null,782}) {783const maxAttempts = repairAttemptLimit(env);784let currentFiles = mergeUniqueStrings(files || []);785let currentAppliedIds = mergeUniqueStrings(appliedEntryIds || []);786let currentFailed = Array.isArray(failed) ? failed : [];787let currentNotes = Array.isArray(notes) ? notes : [];788let currentWarnings = Array.isArray(warnings) ? warnings : [];789let currentFailures = Array.isArray(repairFailures) ? repairFailures : (postChecks?.failures || []);790791for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {792const repair = {793attempt,794maxAttempts,795transactionId: transactionId || null,796reason: repairReason,797failures: summarizeRepairFailures(currentFailures),798files: currentFiles,799pageUrl,800};801let repairResult;802try {803repairResult = await runCopyEditBatchAgent(buildRepairBatch(batch, repair), {804cwd,805provider,806env,807timeoutMs,808applyBatchToSource,809chatAvailable,810});811} catch (err) {812currentFailures = [{813reason: 'repair_agent_failed',814message: err.message || String(err),815}];816continue;817}818819currentFiles = mergeUniqueStrings(currentFiles, repairResult.files || []);820currentNotes = [...currentNotes, ...(repairResult.notes || [])];821currentWarnings = [...currentWarnings, ...(repairResult.warnings || [])];822currentAppliedIds = mergeUniqueStrings(currentAppliedIds, repairResult.appliedEntryIds || []);823currentFailed = mergeFailedEntries(824currentFailed,825normalizeFailedEntries(batch, repairResult, 'repair_failed'),826);827828const verified = verifyEntriesAfterRepair({829batch,830appliedEntryIds: currentAppliedIds,831files: currentFiles,832cwd,833});834if (verified.failed.length > 0) {835currentFailures = verified.failed;836continue;837}838839const repairedChecks = runCopyEditPostApplyChecks({ cwd, files: currentFiles });840currentWarnings = [...currentWarnings, ...(repairedChecks.warnings || [])];841if (!repairedChecks.ok) {842currentFailures = repairedChecks.failures || [];843continue;844}845846const cleared = clearAppliedEntries(cwd, verified.verifiedIds);847const counts = countByPage(cwd);848const verifiedIdSet = new Set(verified.verifiedIds);849return {850applied: summarizeAppliedEntries(batch.entries, verified.verifiedIds),851failed: mergeFailedEntries(currentFailed).filter((item) => !verifiedIdSet.has(item.id)),852files: currentFiles,853cleared,854count,855pageUrl,856warnings: currentWarnings,857notes: currentNotes,858repair: {859status: 'repaired',860attempts: attempt,861maxAttempts,862transactionId: transactionId || null,863},864...counts,865};866}867868const decisionFailedEntries = currentAppliedIds.length > 0869? (batch.entries || [])870.filter((entry) => currentAppliedIds.includes(entry.id))871.map((entry) => ({872id: entry.id,873reason: repairReason,874checks: currentFailures,875candidates: candidatesForEntry(batch, entry.id),876}))877: verificationFailuresForEntries(batch, batch.entries || [], repairReason, { checks: currentFailures });878return {879applied: [],880failed: mergeFailedEntries(decisionFailedEntries, currentFailed),881files: currentFiles,882cleared: 0,883count,884pageUrl,885warnings: currentWarnings,886notes: currentNotes,887reason: 'manual_edit_repair_needs_decision',888needsManualDecision: true,889repair: {890status: 'needs_decision',891attempts: maxAttempts,892maxAttempts,893transactionId: transactionId || null,894failures: summarizeRepairFailures(currentFailures),895files: currentFiles,896},897...countByPage(cwd),898};899}900901export async function commitManualEdits({902cwd = process.cwd(),903pageUrl = null,904provider = undefined,905env = process.env,906timeoutMs = undefined,907applyBatchToSource = undefined,908chatAvailable = undefined,909repairOnly = false,910transactionId = null,911batch: providedBatch = null,912} = {}) {913try {914readBufferStrict(cwd);915} catch (err) {916return {917applied: [],918failed: [],919files: [],920cleared: 0,921count: 0,922pageUrl,923reason: 'manual_edit_buffer_invalid',924message: err.message || String(err),925...countByPage(cwd),926};927}928929const batch = providedBatch || buildManualEditEvidence({ cwd, pageUrl });930const count = countOps(batch.entries);931if (count === 0) {932return {933applied: [],934failed: [],935files: [],936cleared: 0,937count: 0,938pageUrl,939reason: 'no_pending_edits',940...countByPage(cwd),941};942}943944const baseRollbackScope = collectApplyOwnedFiles(batch, cwd);945const rollbackSnapshot = snapshotRollbackFiles(cwd, baseRollbackScope);946let result;947try {948result = repairOnly949? {950status: 'done',951appliedEntryIds: allEntryIds(batch),952failed: [],953files: collectApplyOwnedFiles(batch, cwd),954notes: ['repair-only validation pass'],955}956: await runCopyEditBatchAgent(batch, {957cwd,958provider,959env,960timeoutMs,961applyBatchToSource,962chatAvailable,963});964} catch (err) {965const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, [], baseRollbackScope);966return {967applied: [],968failed: batch.entries.map((entry) => ({969id: entry.id,970reason: err.message || String(err),971candidates: candidatesForEntry(batch, entry.id),972})),973files: [],974cleared: 0,975count,976pageUrl,977rolledBackFiles: rollback.rolledBackFiles,978rollbackFailures: rollback.rollbackFailures,979...countByPage(cwd),980};981}982983if (result.status === 'error') {984const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);985const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);986const failed = normalizeFailedEntries(batch, result, result.message || 'AI copy edit failed');987return {988applied: [],989failed: failed.length > 0990? failed991: verificationFailuresForEntries(batch, batch.entries, result.message || 'AI copy edit failed'),992files: result.files || [],993cleared: 0,994count,995pageUrl,996notes: result.notes || [],997rolledBackFiles: rollback.rolledBackFiles,998rollbackFailures: rollback.rollbackFailures,999...countByPage(cwd),1000};1001}10021003const reportedAppliedIds = uniqueStrings(result.appliedEntryIds || []);1004const reportedFiles = uniqueStrings(result.files || [])1005.map((file) => normalizeRelativeFile(cwd, file))1006.filter(Boolean);1007const aiFailed = normalizeFailedEntries(batch, result, 'AI copy edit failed');1008const rollbackScope = collectApplyOwnedFiles(batch, cwd, result.files || []);1009const failedIds = new Set(aiFailed.map((item) => item.id).filter(Boolean));1010const conflictingAppliedIds = reportedAppliedIds.filter((id) => failedIds.has(id));10111012if (conflictingAppliedIds.length > 0) {1013const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);1014const conflictingEntries = batch.entries.filter((entry) => conflictingAppliedIds.includes(entry.id));1015return {1016applied: [],1017failed: [1018...verificationFailuresForEntries(batch, conflictingEntries, 'conflicting_apply_result'),1019...aiFailed.filter((item) => !conflictingAppliedIds.includes(item.id)),1020],1021files: result.files || [],1022cleared: 0,1023count,1024pageUrl,1025notes: result.notes || [],1026rolledBackFiles: rollback.rolledBackFiles,1027rollbackFailures: rollback.rollbackFailures,1028...countByPage(cwd),1029};1030}10311032const unreportedFiles = unreportedChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);1033if (unreportedFiles.length > 0) {1034const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], [...rollbackScope, ...unreportedFiles]);1035return {1036applied: [],1037failed: verificationFailuresForEntries(batch, batch.entries, 'unreported_source_changes', { files: unreportedFiles }),1038files: result.files || [],1039unreportedFiles,1040cleared: 0,1041count,1042pageUrl,1043notes: result.notes || [],1044rolledBackFiles: rollback.rolledBackFiles,1045rollbackFailures: rollback.rollbackFailures,1046...countByPage(cwd),1047};1048}10491050if (result.status === 'done' && reportedAppliedIds.length === 0) {1051const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);1052return {1053applied: [],1054failed: verificationFailuresForEntries(batch, batch.entries, 'missing_applied_entry_ids'),1055files: result.files || [],1056cleared: 0,1057count,1058pageUrl,1059notes: result.notes || [],1060rolledBackFiles: rollback.rolledBackFiles,1061rollbackFailures: rollback.rollbackFailures,1062...countByPage(cwd),1063};1064}10651066const reportedAppliedEntries = batch.entries.filter((entry) => reportedAppliedIds.includes(entry.id));1067if (reportedAppliedIds.length > 0 && reportedFiles.length === 0) {1068return repairPostApplyValidation({1069batch,1070cwd,1071pageUrl,1072count,1073provider,1074env,1075timeoutMs,1076applyBatchToSource,1077chatAvailable,1078transactionId,1079appliedEntryIds: reportedAppliedIds,1080files: result.files || [],1081failed: aiFailed,1082notes: result.notes || [],1083warnings: result.warnings || [],1084repairReason: 'missing_touched_files',1085repairFailures: verificationFailuresForEntries(batch, reportedAppliedEntries, 'missing_touched_files'),1086});1087}10881089const verifiedAppliedIds = [];1090const verificationFailed = [];1091for (const entry of reportedAppliedEntries) {1092const failures = verifyAppliedEntry({ batch, entry, reportedFiles, cwd });1093if (failures.length === 0) {1094verifiedAppliedIds.push(entry.id);1095} else {1096verificationFailed.push({1097id: entry.id,1098reason: 'source_verification_failed',1099failures,1100candidates: candidatesForEntry(batch, entry.id),1101});1102}1103}1104const unreportedEntries = result.status === 'done' || result.status === 'partial'1105? batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id) && !aiFailed.some((item) => item.id === entry.id))1106: [];1107const nonRepairFailed = [1108...verificationFailuresForEntries(batch, unreportedEntries, 'not_reported_applied'),1109...aiFailed,1110];1111const failed = [1112...verificationFailed,1113...nonRepairFailed,1114];11151116const unappliedEntries = batch.entries.filter((entry) => !reportedAppliedIds.includes(entry.id));1117const leakedUnapplied = findUnappliedEntrySourceChanges({1118batch,1119entries: unappliedEntries,1120reportedFiles,1121cwd,1122rollbackSnapshot,1123});1124if (leakedUnapplied.length > 0) {1125const leakedIds = new Set(leakedUnapplied.map((item) => item.id).filter(Boolean));1126const rolledBackVerified = reportedAppliedEntries1127.filter((entry) => verifiedAppliedIds.includes(entry.id))1128.map((entry) => ({1129id: entry.id,1130reason: 'rolled_back_due_to_failed_entry_source_changed',1131candidates: candidatesForEntry(batch, entry.id),1132}));1133const rollback = rollbackChangedFiles(cwd, rollbackSnapshot, result.files || [], rollbackScope);1134return {1135applied: [],1136failed: [1137...leakedUnapplied,1138...failed.filter((item) => !leakedIds.has(item.id)),1139...rolledBackVerified,1140],1141files: result.files || [],1142cleared: 0,1143count,1144pageUrl,1145rolledBackFiles: rollback.rolledBackFiles,1146rollbackFailures: rollback.rollbackFailures,1147notes: result.notes || [],1148...countByPage(cwd),1149};1150}11511152if (verificationFailed.length > 0) {1153return repairPostApplyValidation({1154batch,1155cwd,1156pageUrl,1157count,1158provider,1159env,1160timeoutMs,1161applyBatchToSource,1162chatAvailable,1163transactionId,1164appliedEntryIds: reportedAppliedIds,1165files: result.files || [],1166failed: nonRepairFailed,1167notes: result.notes || [],1168warnings: result.warnings || [],1169repairReason: 'source_verification_failed',1170repairFailures: verificationFailed,1171});1172}11731174const postChecks = runCopyEditPostApplyChecks({ cwd, files: result.files || [] });1175if (!postChecks.ok) {1176const postCheckEntries = verifiedAppliedIds.length > 01177? reportedAppliedEntries.filter((entry) => verifiedAppliedIds.includes(entry.id))1178: batch.entries;1179return repairPostApplyValidation({1180batch,1181cwd,1182pageUrl,1183count,1184provider,1185env,1186timeoutMs,1187applyBatchToSource,1188chatAvailable,1189transactionId,1190appliedEntryIds: verifiedAppliedIds.length > 01191? verifiedAppliedIds1192: postCheckEntries.map((entry) => entry.id).filter(Boolean),1193files: result.files || [],1194failed,1195notes: result.notes || [],1196warnings: [...(result.warnings || []), ...(postChecks.warnings || [])],1197postChecks,1198});1199}12001201const cleared = clearAppliedEntries(cwd, verifiedAppliedIds);1202const counts = countByPage(cwd);1203return {1204applied: summarizeAppliedEntries(batch.entries, verifiedAppliedIds),1205failed,1206files: result.files || [],1207cleared,1208count,1209pageUrl,1210warnings: [...(result.warnings || []), ...(postChecks.warnings || [])],1211notes: result.notes || [],1212...counts,1213};1214}12151216async function main() {1217const args = process.argv.slice(2);1218if (args.includes('--help') || args.includes('-h')) {1219console.log('Usage: node live-commit-manual-edits.mjs [--page-url=<url>] [--provider=auto|codex|claude|mock]');1220process.exit(0);1221}12221223const result = await commitManualEdits({1224cwd: process.cwd(),1225pageUrl: argVal(args, '--page-url'),1226provider: argVal(args, '--provider') || undefined,1227timeoutMs: Number(process.env.IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS || 120000),1228});1229console.log(JSON.stringify(result));1230}12311232if (process.argv[1]?.endsWith('live-commit-manual-edits.mjs')) {1233main().catch((err) => {1234console.error(JSON.stringify({ error: 'commit_failed', message: err.message || String(err) }));1235process.exit(1);1236});1237}12381239function escapeRegExp(value) {1240return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');1241}1242