Bound copy-edit prompt context

Whitelist and truncate staged operation context before it reaches the local agent prompt.

AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
This commit is contained in:
Paul Bakaus
2026-08-06 09:22:41 -07:00
parent a075d89bdb
commit 6e6b0227b0
2 changed files with 89 additions and 4 deletions
+46 -4
View File
@@ -14,6 +14,7 @@ import path from 'node:path';
import { createRequire } from 'node:module';
const DEFAULT_TIMEOUT_MS = 60_000;
const BATCH_OP_TEXT_LIMIT = 240;
const require = createRequire(import.meta.url);
export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) {
@@ -311,18 +312,59 @@ function compactBatchOp(op) {
contextRef: op.contextRef,
tag: op.tag,
elementId: op.elementId,
classes: op.classes,
classes: compactBatchStringList(op.classes, 24),
originalText: op.originalText,
newText: op.newText,
deleted: op.deleted === true || undefined,
sourceHint: op.sourceHint,
sourceHint: normalizeBatchSourceHint(op.sourceHint),
leaf: compactContextForBatch(op.leaf),
nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [],
nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts),
container: compactContextForBatch(op.container),
contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [],
contextHints: compactBatchStringList(op.contextHints, 12),
};
}
function normalizeBatchSourceHint(hint) {
if (!hint || typeof hint !== 'object') return null;
let line = Number.isFinite(Number(hint.line)) ? Number(hint.line) : null;
let column = Number.isFinite(Number(hint.column)) ? Number(hint.column) : null;
if ((!line || !column) && typeof hint.loc === 'string') {
const match = hint.loc.match(/^(\d+)(?::(\d+))?/);
if (match) {
line = Number(match[1]);
if (match[2]) column = Number(match[2]);
}
}
return {
file: compactBatchString(hint.file) || '',
loc: compactBatchString(hint.loc) || '',
line,
column,
};
}
function compactNearbyBatchTexts(items) {
return (Array.isArray(items) ? items : [])
.slice(0, 8)
.map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : {
ref: compactBatchString(item?.ref),
tag: compactBatchString(item?.tag),
classes: compactBatchStringList(item?.classes, 24),
text: compactBatchString(item?.text),
});
}
function compactBatchStringList(items, limit) {
return (Array.isArray(items) ? items : [])
.slice(0, limit)
.filter((item) => typeof item === 'string')
.map((item) => truncate(item, BATCH_OP_TEXT_LIMIT));
}
function compactBatchString(value) {
return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined;
}
function compactContextForBatch(value) {
if (!value || typeof value !== 'object') return value || null;
return {
+43
View File
@@ -51,6 +51,49 @@ describe('live-copy-edit-agent', () => {
assert.match(prompt, /Return ONLY JSON/);
});
it('bounds and whitelists operation context in batch prompts', () => {
const huge = 'Z'.repeat(50_000);
const prompt = buildCopyEditBatchPrompt({
pageUrl: '/',
entries: [{
id: 'bounded',
pageUrl: '/',
ops: [{
classes: [huge],
originalText: 'Old',
newText: 'New',
sourceHint: {
file: 'src/App.jsx',
loc: '12:3',
nested: { payload: huge },
},
nearbyEditableTexts: [{
ref: 'body>main>span',
tag: 'span',
classes: ['label'],
text: huge,
extra: huge,
}],
contextHints: [huge],
}],
}],
});
const serializedBatch = prompt.split('Staged copy-edit batch:\n').pop();
const op = JSON.parse(serializedBatch).entries[0].ops[0];
assert.ok(prompt.length < 20_000, `expected compact prompt, got ${prompt.length} characters`);
assert.ok(op.classes[0].length < 400);
assert.deepEqual(op.sourceHint, {
file: 'src/App.jsx',
loc: '12:3',
line: 12,
column: 3,
});
assert.deepEqual(Object.keys(op.nearbyEditableTexts[0]).sort(), ['classes', 'ref', 'tag', 'text']);
assert.ok(op.nearbyEditableTexts[0].text.length < 400);
assert.ok(op.contextHints[0].length < 400);
});
it('parses partial batch results', () => {
assert.deepEqual(
parseCopyEditBatchResult('{"status":"partial","appliedEntryIds":["a"],"failed":[{"entryId":"b","reason":"ambiguous"}],"files":["src/page.js"]}'),