synergyspec-selfevolving 2.1.7 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -423,13 +423,14 @@ function packageHostEdits(editsInput, allowedFiles, currentFiles, group, targetI
|
|
|
423
423
|
}
|
|
424
424
|
const validated = validateCandidateEdits(editsInput.edits, allowedFiles);
|
|
425
425
|
const oldByPath = new Map(currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
426
|
-
// No-op guard:
|
|
427
|
-
// nothing to evolve — surface it as a no-op
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
// No-op guard: package only edits whose content differs from the current
|
|
427
|
+
// file. If none remain there is nothing to evolve — surface it as a no-op
|
|
428
|
+
// (placeholder), like the agent path.
|
|
429
|
+
const changed = validated.filter((e) => (oldByPath.get(e.relPath) ?? '') !== e.content);
|
|
430
|
+
if (changed.length === 0) {
|
|
430
431
|
throw new EvolvingAgentNoOp();
|
|
431
432
|
}
|
|
432
|
-
const diffPatch =
|
|
433
|
+
const diffPatch = changed
|
|
433
434
|
.map((e) => renderUnifiedDiff(e.relPath, oldByPath.get(e.relPath) ?? '', e.content))
|
|
434
435
|
.join('\n');
|
|
435
436
|
const rationale = (editsInput.rationale ?? '').trim() ||
|
|
@@ -437,9 +438,9 @@ function packageHostEdits(editsInput, allowedFiles, currentFiles, group, targetI
|
|
|
437
438
|
return {
|
|
438
439
|
targetId,
|
|
439
440
|
diffPatch,
|
|
440
|
-
changedFiles:
|
|
441
|
+
changedFiles: changed.map((e) => e.relPath),
|
|
441
442
|
rationale,
|
|
442
|
-
edits:
|
|
443
|
+
edits: changed,
|
|
443
444
|
};
|
|
444
445
|
}
|
|
445
446
|
/**
|
|
@@ -53,6 +53,9 @@ export interface CanonicalProposeOutput {
|
|
|
53
53
|
* {@link EvolvingAgentOutputInvalid} for any shape / frozen / scope
|
|
54
54
|
* violation. Path traversal and absolute paths are rejected transitively: they
|
|
55
55
|
* can never be a member of `allowedFiles`, so they fail the scope check.
|
|
56
|
+
* Duplicate relPaths are rejected because full-file replacements have no
|
|
57
|
+
* stable sequencing semantics; allowing them would let a later entry silently
|
|
58
|
+
* override an earlier one.
|
|
56
59
|
*/
|
|
57
60
|
export declare function validateCandidateEdits(rawEdits: readonly unknown[], allowedFiles: readonly string[]): {
|
|
58
61
|
relPath: string;
|
|
@@ -58,6 +58,9 @@ export const CanonicalProposerInvocationError = EvolvingAgentInvocationError;
|
|
|
58
58
|
* {@link EvolvingAgentOutputInvalid} for any shape / frozen / scope
|
|
59
59
|
* violation. Path traversal and absolute paths are rejected transitively: they
|
|
60
60
|
* can never be a member of `allowedFiles`, so they fail the scope check.
|
|
61
|
+
* Duplicate relPaths are rejected because full-file replacements have no
|
|
62
|
+
* stable sequencing semantics; allowing them would let a later entry silently
|
|
63
|
+
* override an earlier one.
|
|
61
64
|
*/
|
|
62
65
|
export function validateCandidateEdits(rawEdits, allowedFiles) {
|
|
63
66
|
if (rawEdits.length === 0) {
|
|
@@ -66,6 +69,7 @@ export function validateCandidateEdits(rawEdits, allowedFiles) {
|
|
|
66
69
|
const allowed = new Set(allowedFiles.map((p) => p.replace(/\\/g, '/')));
|
|
67
70
|
const frozen = new Set(GATE_DEFINING_FILES.map((p) => p.replace(/\\/g, '/')));
|
|
68
71
|
const validated = [];
|
|
72
|
+
const seen = new Set();
|
|
69
73
|
for (const e of rawEdits) {
|
|
70
74
|
if (!e || typeof e !== 'object') {
|
|
71
75
|
throw new EvolvingAgentOutputInvalid('edit entry must be an object');
|
|
@@ -82,6 +86,10 @@ export function validateCandidateEdits(rawEdits, allowedFiles) {
|
|
|
82
86
|
if (!allowed.has(norm)) {
|
|
83
87
|
throw new EvolvingAgentOutputInvalid(`edit relPath "${relPath}" is outside the target's declared files`);
|
|
84
88
|
}
|
|
89
|
+
if (seen.has(norm)) {
|
|
90
|
+
throw new EvolvingAgentOutputInvalid(`edit relPath "${relPath}" appears more than once; combine changes into one full-file content edit`);
|
|
91
|
+
}
|
|
92
|
+
seen.add(norm);
|
|
85
93
|
validated.push({ relPath: norm, content });
|
|
86
94
|
}
|
|
87
95
|
return validated;
|
|
@@ -111,6 +111,8 @@ function preludeLines(editBudget) {
|
|
|
111
111
|
'- Only edit files listed under "CANONICAL TARGET" below. Never invent paths.',
|
|
112
112
|
'- Prefer compact edit operations: `replaceText`, `insertAfter`, or `insertBefore`.',
|
|
113
113
|
'- `replaceText.find` and marker text must match exactly one location in the current file.',
|
|
114
|
+
'- For `insertAfter`/`insertBefore`, include a non-empty `marker` field and a string `insert` field.',
|
|
115
|
+
'- Empty `edits` is valid only with a non-empty `refusal`; never emit `{"edits":[]}` alone.',
|
|
114
116
|
'- Include needed newlines inside `replace`/`insert`; the runner will expand the operation locally.',
|
|
115
117
|
'- Legacy `content` full-file replacements are accepted only for small files or true rewrites.',
|
|
116
118
|
'- Do not re-emit a large canonical file just to make a small localized edit.',
|
|
@@ -253,6 +255,9 @@ export function parseEvolvingAgentResponse(text) {
|
|
|
253
255
|
// Empty edits with no refusal reason is a malformed no-op, not a refusal.
|
|
254
256
|
throw new EvolvingAgentNoOp();
|
|
255
257
|
}
|
|
258
|
+
if (Object.prototype.hasOwnProperty.call(o, 'refusal')) {
|
|
259
|
+
throw new EvolvingAgentOutputInvalid('refusal is only valid with empty edits; choose either a refusal or a concrete edit');
|
|
260
|
+
}
|
|
256
261
|
// Concrete-edit shape: validate prediction + edit instruction shapes.
|
|
257
262
|
const prediction = parsePrediction(o.prediction);
|
|
258
263
|
const edits = [];
|
|
@@ -339,6 +344,7 @@ function findExactlyOnce(content, needle, description) {
|
|
|
339
344
|
export function resolveEvolvingAgentEditInstructions(candidate, currentFiles) {
|
|
340
345
|
const currentByPath = new Map(currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
341
346
|
const proposedByPath = new Map();
|
|
347
|
+
const editModeByPath = new Map();
|
|
342
348
|
const order = [];
|
|
343
349
|
const rememberPath = (relPath) => {
|
|
344
350
|
if (!proposedByPath.has(relPath))
|
|
@@ -346,6 +352,15 @@ export function resolveEvolvingAgentEditInstructions(candidate, currentFiles) {
|
|
|
346
352
|
};
|
|
347
353
|
for (const raw of candidate.edits) {
|
|
348
354
|
const relPath = raw.relPath.replace(/\\/g, '/');
|
|
355
|
+
const mode = 'content' in raw ? 'fullContent' : 'compact';
|
|
356
|
+
const previousMode = editModeByPath.get(relPath);
|
|
357
|
+
if (mode === 'fullContent' && previousMode !== undefined) {
|
|
358
|
+
throw new EvolvingAgentOutputInvalid(`legacy full-file content edit for "${relPath}" cannot be combined with other edits for the same file`);
|
|
359
|
+
}
|
|
360
|
+
if (mode === 'compact' && previousMode === 'fullContent') {
|
|
361
|
+
throw new EvolvingAgentOutputInvalid(`legacy full-file content edit for "${relPath}" cannot be combined with other edits for the same file`);
|
|
362
|
+
}
|
|
363
|
+
editModeByPath.set(relPath, mode);
|
|
349
364
|
if ('content' in raw) {
|
|
350
365
|
rememberPath(relPath);
|
|
351
366
|
proposedByPath.set(relPath, raw.content);
|
|
@@ -377,6 +392,16 @@ export function resolveEvolvingAgentEditInstructions(candidate, currentFiles) {
|
|
|
377
392
|
edits: order.map((relPath) => ({ relPath, content: proposedByPath.get(relPath) ?? '' })),
|
|
378
393
|
};
|
|
379
394
|
}
|
|
395
|
+
function dropByteIdenticalEdits(candidate, currentFiles) {
|
|
396
|
+
const currentByPath = new Map(currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
397
|
+
return {
|
|
398
|
+
...candidate,
|
|
399
|
+
edits: candidate.edits.filter((edit) => {
|
|
400
|
+
const current = currentByPath.get(edit.relPath.replace(/\\/g, '/'));
|
|
401
|
+
return current === undefined || current !== edit.content;
|
|
402
|
+
}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
380
405
|
function parsePrediction(raw) {
|
|
381
406
|
if (!raw || typeof raw !== 'object') {
|
|
382
407
|
throw new EvolvingAgentOutputInvalid('a concrete edit requires a `prediction` object {metric, direction, checkBy}');
|
|
@@ -613,10 +638,14 @@ export async function runEvolvingAgent(opts) {
|
|
|
613
638
|
parsed = response;
|
|
614
639
|
break;
|
|
615
640
|
}
|
|
616
|
-
const
|
|
641
|
+
const resolved = resolveEvolvingAgentEditInstructions(response, currentFiles);
|
|
617
642
|
// Static-shape edit: validate scope-to-target + frozen freeze here so a
|
|
618
643
|
// bad path is a REPAIRABLE failure (the evolving agent's repair contract).
|
|
619
|
-
validateCandidateEdits(
|
|
644
|
+
validateCandidateEdits(resolved.edits, allowedFiles);
|
|
645
|
+
const candidate = dropByteIdenticalEdits(resolved, currentFiles);
|
|
646
|
+
if (candidate.edits.length === 0) {
|
|
647
|
+
throw new EvolvingAgentOutputInvalid('edit makes no changes against the current policy head - emit a refusal or a real bounded edit');
|
|
648
|
+
}
|
|
620
649
|
// ≤ L budget (repairable).
|
|
621
650
|
const changed = countChangedLines(candidate.edits, currentFiles);
|
|
622
651
|
if (changed > editBudget) {
|
|
@@ -656,7 +685,8 @@ export async function runEvolvingAgent(opts) {
|
|
|
656
685
|
break;
|
|
657
686
|
}
|
|
658
687
|
catch (err) {
|
|
659
|
-
if (err instanceof EvolvingAgentOutputInvalid
|
|
688
|
+
if ((err instanceof EvolvingAgentOutputInvalid || err instanceof EvolvingAgentNoOp) &&
|
|
689
|
+
attempt < maxRepairAttempts) {
|
|
660
690
|
feedback = gateFeedback(err.message);
|
|
661
691
|
continue;
|
|
662
692
|
}
|