refactor(lib): 讀檔與列留言收進 lib,並讓已整併只認自己打的 +1

「讀一個 --xxx-file 或直接失敗」原本在四支腳本各寫一份,錯誤碼還有三種拼法
(BODY_FILE_NOT_FOUND/BODY_FILE_MISSING/CONTENT_FILE_NOT_FOUND)。同一種情況要有同一個
碼,呼叫端才分辨得出是哪一步壞了。留言分頁的那段咒語也是第三份,一併收成 listIssueComments。

countUnmergedComments 原本接受任何人的 +1,而讀留言那邊只認自己的——兩端對「已整併」的
定義不一致。後果是隊友對決策留言按個讚,未處理留言數就掉到 0,analyze 與 feat 再也不提示,
那則決策永遠不會被收進描述。統一成只認自己打的:別人按讚是「我同意」,不是「已經收進去了」。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 09:19:05 +00:00
co-authored by Claude Opus 5
parent d0e7e569a3
commit ee474d0439
2 changed files with 63 additions and 22 deletions
+61 -14
View File
@@ -663,6 +663,28 @@ function checkTimeTracker(info) {
}
}
/**
* 讀一個由 flag 指定的文字檔。
*
* 「讀一個 --xxx-file 或直接失敗」原本在四支腳本裡各寫一份,錯誤碼還有三種拼法。
* 同一種情況要有同一個碼,呼叫端才分辨得出到底是哪一步壞了。
*
* @param {string} path 檔案路徑
* @param {string} flag 出現在錯誤訊息裡的 flag 名,例如 '--body-file'
* @param {{allowEmpty?: boolean}} options 內容可不可以是空的;預設不可以
* @returns {string}
*/
export function readTextFile(path, flag, { allowEmpty = false } = {}) {
if (!existsSync(path)) {
throw new ScriptError('FILE_NOT_FOUND', `找不到 ${flag} 指定的檔案 ${path}`);
}
const content = readFileSync(path, 'utf8');
if (!allowEmpty && content.trim() === '') {
throw new ScriptError('FILE_EMPTY', `${flag} 指定的檔案 ${path} 是空的`);
}
return content;
}
// ── 議題讀取:兩支抽取腳本共用 ────────────────────────────────────
/**
@@ -697,32 +719,57 @@ export const UNMERGED_COMMENT_NOTE =
* 數出尚未被整併回描述的留言則數。
*
* 抽取契約只讀 body 不讀留言,這個數字是下游判斷「手上的描述是不是過期了」的唯一依據。
* 已整併的留言會被打上 `+1` reaction(由 sdlc-sync 負責標記),而 Gitea 的留言物件
* 不含 reaction,所以只能逐則再查一次。留言多時請求數會跟著長,但這個數字要準
* ——它決定下游會不會拿著過期的描述做事,所以留言也要逐頁讀完,讀不完寧可報錯。
* 已整併的留言會被打上 `+1` reaction(由 sdlc-sync 負責標記)。留言多時請求數會跟著長,
* 但這個數字要準——它決定下游會不會拿著過期的描述做事,所以留言也要逐頁讀完,
* 讀不完寧可報錯。
*
* @param {{base: string, token: string}} login
* @param {string} repo owner/name
* @param {number} index
* @param {string} me 目前登入帳號:只有自己打的 `+1` 才算整併過
* @returns {Promise<number>}
*/
export async function countUnmergedComments(login, repo, index) {
const commentsPath = `/repos/${repo}/issues/${index}/comments`;
export async function countUnmergedComments(login, repo, index, me) {
let unmerged = 0;
for await (const comments of pages(login, commentsPath, {
limitCode: 'COMMENT_LIMIT',
limitHint: `${commentsPath} 的留言太多,數不完未整併的則數`,
})) {
for (const comment of comments) {
const path = `/repos/${repo}/issues/comments/${comment.id}/reactions`;
const reactions = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? [];
if (!reactions.some((reaction) => reaction.content === '+1')) unmerged += 1;
}
for await (const comment of listIssueComments(login, repo, index)) {
if (!(await mergedByMe(login, repo, comment.id, me))) unmerged += 1;
}
return unmerged;
}
/**
* 逐頁走過一顆議題(或 PR)的一般留言。
* 三支腳本都要做這件事:數未整併的則數、列出留言內容、核對 --merged 的 id。
* @returns {AsyncGenerator<object>} 一則一則交出去
*/
export async function* listIssueComments(login, repo, index) {
const path = `/repos/${repo}/issues/${index}/comments`;
for await (const comments of pages(login, path, {
limitCode: 'COMMENT_LIMIT',
limitHint: `${path} 的留言太多,讀不完整份清單`,
})) {
for (const comment of comments) yield comment;
}
}
/**
* 這一則是不是「我」標記過已整併。
*
* Gitea 的留言物件不含 reaction,只能逐則再查一次。認的是自己打的 `+1`:
* 別人按讚是「我同意」,當成已整併會讓那一則的決策永遠不被收進描述——
* 而那正是 /sdlc-sync 要解決的事。
*
* @param {string} me 目前登入帳號;preflight 的回傳帶得出來
*/
export async function mergedByMe(login, repo, id, me) {
const path = `/repos/${repo}/issues/comments/${id}/reactions`;
const reactions = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? [];
return reactions.some((reaction) => reaction.content === '+1' && reaction.user?.login === me);
}
// ── 碼錶 ───────────────────────────────────────────────────────────
/**