Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aea3e93d36 | |||
| c66a9aa025 | |||
| 79e4042003 | |||
| 92f10c7970 | |||
| 07e38f9d45 | |||
| 49f190e944 | |||
| 72701dee0a | |||
| 503e50a2d0 | |||
| dddcc9031b | |||
| ace50037ba | |||
| 76eaff7788 | |||
| 6ac8512dbc | |||
| 3b8e942e7f | |||
| 051457b11b | |||
| 92f1c6fe82 | |||
| 27df6894a4 | |||
| 1afd978059 | |||
| 146faca7cb | |||
| 4c99247566 | |||
| 81cbb83340 | |||
| 3f65b72cf0 | |||
| 2eb94c8f74 | |||
| 6354c0987c | |||
| 7df34eb1d0 | |||
| ca5d54882f | |||
| ca4664e0cc |
@@ -203,6 +203,10 @@ export async function mergeInstructionText(existingText, sourceText, relPath, ai
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const aiMerged = await aiMergeAssistant({ relPath, existingText, sourceText, deterministicText: deterministic, requiredBlocks });
|
const aiMerged = await aiMergeAssistant({ relPath, existingText, sourceText, deterministicText: deterministic, requiredBlocks });
|
||||||
|
if (aiMerged == null) {
|
||||||
|
warn(`[merge] ${relPath} AI result unavailable; using deterministic merge`);
|
||||||
|
return deterministic;
|
||||||
|
}
|
||||||
if (typeof aiMerged === 'string' && validateMergedInstructionText(aiMerged, requiredBlocks)) {
|
if (typeof aiMerged === 'string' && validateMergedInstructionText(aiMerged, requiredBlocks)) {
|
||||||
return normalizeText(aiMerged) === normalizeText(existingText) ? existingText : aiMerged;
|
return normalizeText(aiMerged) === normalizeText(existingText) ? existingText : aiMerged;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,6 +211,15 @@ describe('commitAndPush', () => {
|
|||||||
assert.ok(result.includes('extra block'));
|
assert.ok(result.includes('extra block'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses deterministic instruction merge when AI returns no usable result', async () => {
|
||||||
|
const aiMergeAssistant = async () => null;
|
||||||
|
|
||||||
|
const result = await mergeInstructionText('repo block', 'source block', 'AGENTS.md', aiMergeAssistant);
|
||||||
|
|
||||||
|
assert.ok(result.includes('repo block'));
|
||||||
|
assert.ok(result.includes('source block'));
|
||||||
|
});
|
||||||
|
|
||||||
it('exits when AI output drops a block', async () => {
|
it('exits when AI output drops a block', async () => {
|
||||||
const originalExit = process.exit;
|
const originalExit = process.exit;
|
||||||
let exitCode = null;
|
let exitCode = null;
|
||||||
|
|||||||
+61
-1
@@ -111,9 +111,69 @@ export async function chat(systemPrompt, userContent) {
|
|||||||
export async function chatJSON(systemPrompt, userContent) {
|
export async function chatJSON(systemPrompt, userContent) {
|
||||||
const text = await chat(systemPrompt, userContent);
|
const text = await chat(systemPrompt, userContent);
|
||||||
try {
|
try {
|
||||||
return JSON.parse(text.trim().replace(/^```[^\n]*\n?/, '').replace(/```$/, '').trim());
|
return JSON.parse(extractJSONText(text));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
line(`[LLM] JSON 解析失敗: ${e.message}`);
|
line(`[LLM] JSON 解析失敗: ${e.message}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripOuterFence(text) {
|
||||||
|
return String(text)
|
||||||
|
.trim()
|
||||||
|
.replace(/^```[a-zA-Z0-9_-]*\n?/, '')
|
||||||
|
.replace(/```$/, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBalancedJSON(text, startIndex) {
|
||||||
|
const source = String(text);
|
||||||
|
const open = source[startIndex];
|
||||||
|
const close = open === '{' ? '}' : ']';
|
||||||
|
let depth = 0;
|
||||||
|
let inString = false;
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
for (let i = startIndex; i < source.length; i++) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (ch === '\\') {
|
||||||
|
escaped = true;
|
||||||
|
} else if (ch === '"') {
|
||||||
|
inString = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"') {
|
||||||
|
inString = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === open) depth += 1;
|
||||||
|
else if (ch === close) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) return source.slice(startIndex, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractJSONText(text) {
|
||||||
|
const stripped = stripOuterFence(text);
|
||||||
|
try {
|
||||||
|
JSON.parse(stripped);
|
||||||
|
return stripped;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
for (let i = 0; i < stripped.length; i++) {
|
||||||
|
if (stripped[i] !== '[' && stripped[i] !== '{') continue;
|
||||||
|
const candidate = extractBalancedJSON(stripped, i);
|
||||||
|
if (!candidate) continue;
|
||||||
|
try {
|
||||||
|
JSON.parse(candidate);
|
||||||
|
return candidate;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,6 +208,20 @@ describe('chatJSON', async () => {
|
|||||||
assert.deepEqual(result, [{ level: 'info' }]);
|
assert.deepEqual(result, [{ level: 'info' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('extracts JSON array from surrounding prose', async () => {
|
||||||
|
process.env.OPENAI_API_KEY = 'sk-test';
|
||||||
|
mockAxiosPost([makeOkResponse('**Reviewing findings**\n\n[{"level":"warning","suggestion":"x"}]\n\nDone.')]);
|
||||||
|
const result = await chatJSON('sys', 'user');
|
||||||
|
assert.deepEqual(result, [{ level: 'warning', suggestion: 'x' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts JSON object from surrounding prose', async () => {
|
||||||
|
process.env.OPENAI_API_KEY = 'sk-test';
|
||||||
|
mockAxiosPost([makeOkResponse('**Begin Combine**\n{"merged_text":"repo block\\n\\nsource block"}')]);
|
||||||
|
const result = await chatJSON('sys', 'user');
|
||||||
|
assert.deepEqual(result, { merged_text: 'repo block\n\nsource block' });
|
||||||
|
});
|
||||||
|
|
||||||
it('returns [] when JSON is invalid', async () => {
|
it('returns [] when JSON is invalid', async () => {
|
||||||
process.env.OPENAI_API_KEY = 'sk-test';
|
process.env.OPENAI_API_KEY = 'sk-test';
|
||||||
mockAxiosPost([makeOkResponse('not json')]);
|
mockAxiosPost([makeOkResponse('not json')]);
|
||||||
|
|||||||
Reference in New Issue
Block a user