feat(議題解析): 換掉一個段落的內容,標題與其餘段落一字不動
整併留言裡的決策時用它。不整份重寫的理由跟 upsertLineInSection 一樣,只是代價更大: 重寫會把別人在其他段落的編輯一起蓋掉,而議題的編輯紀錄沒有人會去比對。 三件事照著同檔既有的規矩做:圍欄裡的假標題不算段落;同名標題出現不只一次時交回 ambiguous 而不賭第一個(蓋掉的是一整段,猜錯的代價比 tickLine 更高);換行沿用 body 原本的那一種, CRLF 的 body 裡混進 LF 會讓抽取契約交出的 raw 對不上原文,之後就勾不動那幾行。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -338,6 +338,65 @@ export function tickLine(body, raw, section) {
|
|||||||
return { status: 'ticked', body: lines.join('\n'), line: ticked, count: 1 };
|
return { status: 'ticked', body: lines.join('\n'), line: ticked, count: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 換掉一個段落的內容,標題與其餘段落一字不動。
|
||||||
|
*
|
||||||
|
* 整併留言裡的決策時用它。不整份重寫的理由跟 upsertLineInSection 一樣,只是代價更大:
|
||||||
|
* 重寫會把別人在其他段落的編輯一起蓋掉,而議題的編輯紀錄沒有人會去比對。
|
||||||
|
*
|
||||||
|
* 同名標題出現不只一次時交回 `ambiguous`,不賭第一個——理由與 tickLine 相同,
|
||||||
|
* 而這裡蓋掉的是一整段而不是一行,猜錯的代價更高。
|
||||||
|
*
|
||||||
|
* 段落不存在時交回 `not-found` 讓呼叫端報錯,不補在結尾:「找不到那一段」多半是段落名
|
||||||
|
* 打錯,這時把內容塞到議題末尾,比什麼都不做更難收拾。
|
||||||
|
*
|
||||||
|
* @param {string} body 議題 body
|
||||||
|
* @param {string} section 段落名稱,例如 '目標'
|
||||||
|
* @param {string} content 新的段落內容(不含 `## 標題` 那一行)
|
||||||
|
* @returns {{status: 'replaced'|'not-found'|'ambiguous', body?: string, count: number}}
|
||||||
|
*/
|
||||||
|
export function replaceSection(body, section, content) {
|
||||||
|
const rows = [...eachLine(body)];
|
||||||
|
const headings = [];
|
||||||
|
for (let i = 0; i < rows.length; i += 1) {
|
||||||
|
if (rows[i].inFence) continue;
|
||||||
|
if (rows[i].line.match(/^##\s+(.+?)\s*$/)?.[1] === section) headings.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headings.length === 0) return { status: 'not-found', count: 0 };
|
||||||
|
if (headings.length > 1) return { status: 'ambiguous', count: headings.length };
|
||||||
|
|
||||||
|
const [start] = headings;
|
||||||
|
const lines = rows.map((row) => row.line);
|
||||||
|
// 下一個段落的標題;沒有就是到結尾
|
||||||
|
let end = lines.length;
|
||||||
|
for (let i = start + 1; i < lines.length; i += 1) {
|
||||||
|
if (!rows[i].inFence && /^##\s+/.test(lines[i])) {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 段落與段落之間的空行屬於版面,不屬於內容:換內容時把它留著。
|
||||||
|
// 原本就沒有空行(兩個標題緊貼)時補一個,免得新內容黏在下一個標題上。
|
||||||
|
let tail = end;
|
||||||
|
while (tail > start + 1 && lines[tail - 1].trim() === '') tail -= 1;
|
||||||
|
const spacer = end === lines.length || end > tail ? lines.slice(tail, end) : [''];
|
||||||
|
|
||||||
|
// 換行沿用 body 原本的那一種:CRLF 的 body 裡混進 LF,會讓抽取契約交出的 raw
|
||||||
|
// 對不上原文,之後就勾不動那幾行了
|
||||||
|
const eol = body.includes('\r\n') ? '\r\n' : '\n';
|
||||||
|
const normalized = content.trim().split(/\r?\n/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'replaced',
|
||||||
|
body: [...lines.slice(0, start + 1), '', ...normalized, ...spacer, ...lines.slice(end)]
|
||||||
|
.map((line) => line.replace(/\r$/, ''))
|
||||||
|
.join(eol),
|
||||||
|
count: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在指定段落裡就地更新(或補上)一行「前綴+值」。
|
* 在指定段落裡就地更新(或補上)一行「前綴+值」。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ main(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const login = resolveLogin({ host: flags.host });
|
const login = resolveLogin({ host: flags.host });
|
||||||
await preflight(login, repo);
|
const { user } = await preflight(login, repo);
|
||||||
|
|
||||||
const issue = await fetchIssue(login, repo, index);
|
const issue = await fetchIssue(login, repo, index);
|
||||||
const sections = parseSections(issue.body);
|
const sections = parseSections(issue.body);
|
||||||
@@ -66,7 +66,7 @@ main(async () => {
|
|||||||
驗收標準: listSection(sections, '驗收標準'),
|
驗收標準: listSection(sections, '驗收標準'),
|
||||||
影響範圍: listSection(sections, '影響範圍'),
|
影響範圍: listSection(sections, '影響範圍'),
|
||||||
未決事項: listSection(sections, '未決事項'),
|
未決事項: listSection(sections, '未決事項'),
|
||||||
未處理留言數: await countUnmergedComments(login, repo, index),
|
未處理留言數: await countUnmergedComments(login, repo, index, user.login),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ main(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const login = resolveLogin({ host: flags.host });
|
const login = resolveLogin({ host: flags.host });
|
||||||
await preflight(login, repo);
|
const { user } = await preflight(login, repo);
|
||||||
|
|
||||||
const issue = await fetchIssue(login, repo, index);
|
const issue = await fetchIssue(login, repo, index);
|
||||||
const sections = parseSections(issue.body);
|
const sections = parseSections(issue.body);
|
||||||
@@ -88,7 +88,7 @@ main(async () => {
|
|||||||
相依: { blocks, depends },
|
相依: { blocks, depends },
|
||||||
assignee: issue.assignee?.login ?? null,
|
assignee: issue.assignee?.login ?? null,
|
||||||
碼錶中: await hasRunningStopwatch(login, repo, index),
|
碼錶中: await hasRunningStopwatch(login, repo, index),
|
||||||
未處理留言數: await countUnmergedComments(login, repo, index),
|
未處理留言數: await countUnmergedComments(login, repo, index, user.login),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user