mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
@@ -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 {
|
||||
|
||||
@@ -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"]}'),
|
||||
|
||||
Reference in New Issue
Block a user