Files
tea-sdlc/scripts/wp-extract.js
T
jiantw83andClaude Opus 5 03ce382220 refactor(issue-body): 工作包的歸屬判準收成一個函式
wp-extract 與 wp-list 都在問「這顆工作包掛在哪顆需求底下」。規則寫兩份,
某天只會有一邊被改到,而分岔的樣子是「清單裡看得到、抽取卻說不是」。
順手把測試裡兩種取段落的寫法統一,並刪掉沒有人傳過的參數。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:19:33 +08:00

123 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 把一顆工作包議題抽成實作階段要用的精簡 JSON。
*
* 與需求議題的抽取(issue-extract)同樣「只讀 body 不讀留言」,但多做三件事:
* 1. 待辦是巢狀的——每一項待辦底下掛它自己的驗收,並各自帶回未經修改的 `raw`,
* 下游靠 `raw` 做精確字串替換來勾選 checkbox,只改那一行,不重寫整份 body。
* 2. 介面契約是四欄表格,四欄都要留著。
* 3. body 說不出的三個活狀態要現查:相依、領取人、碼錶。
*
* 用法:
* node scripts/wp-extract.js --repo owner/name --index 9 [--host <網址>] [--dry-run]
*/
import {
UNMERGED_COMMENT_NOTE,
countUnmergedComments,
fetchIssue,
listStopwatches,
main,
pages,
parseFlags,
parseIndex,
parseRepo,
preflight,
resolveLogin,
stopwatchOnIssue,
} from './lib.js';
import {
checklistInSection,
listSection,
parseSections,
requirementIndex,
tableRows,
textSection,
} from './issue-body.js';
/** 介面契約表格由左到右的四欄 */
const CONTRACT_COLUMNS = ['介面', '產出者', '消費者', '形狀'];
main(async () => {
const flags = parseFlags(process.argv.slice(2), {
required: ['repo', 'index'],
optional: ['host'],
booleans: ['dry-run'],
});
const repo = parseRepo(flags.repo);
const index = parseIndex(flags.index);
const issuePath = `/repos/${repo}/issues/${index}`;
if (flags['dry-run']) {
return {
dryRun: true,
repo,
index,
requests: [
{ method: 'GET', path: issuePath },
{ method: 'GET', path: `${issuePath}/dependencies` },
{ method: 'GET', path: `${issuePath}/blocks` },
{ method: 'GET', path: '/user/stopwatches' },
{ method: 'GET', path: `${issuePath}/comments` },
],
note: UNMERGED_COMMENT_NOTE,
};
}
const login = resolveLogin({ host: flags.host });
const { user } = await preflight(login, repo);
const issue = await fetchIssue(login, repo, index);
const sections = parseSections(issue.body);
// 先讀完兩種相依再組輸出:欄位順序照契約寫成 {blocks, depends},
// 但請求順序維持「先問誰擋著我」,與 --dry-run 預告的一致。
const depends = await fetchLinked(login, `${issuePath}/dependencies`, '先決');
const blocks = await fetchLinked(login, `${issuePath}/blocks`, '阻擋');
return {
index: issue.number,
url: issue.html_url,
title: issue.title,
需求議題: requirementIndex(sections),
描述: textSection(sections, '描述'),
架構圖: textSection(sections, '架構圖'),
範圍邊界: listSection(sections, '範圍邊界'),
介面契約: tableRows(sections, '介面契約', CONTRACT_COLUMNS),
待辦: checklistInSection(issue.body, '待辦'),
整體驗收: listSection(sections, '整體驗收'),
repos: listSection(sections, 'repo 列表'),
相依: { blocks, depends },
assignee: issue.assignee?.login ?? null,
碼錶中: await hasRunningStopwatch(login, repo, index),
未處理留言數: await countUnmergedComments(login, repo, index, user.login),
};
});
/**
* 讀一種相依關係上的議題編號。
* 逐頁讀完:半份清單會讓下游把實作順序排錯,那比直接報錯更難發現。
* @param {string} kind 出現在錯誤訊息裡的關係名稱
*/
async function fetchLinked(login, path, kind) {
const indexes = [];
for await (const issues of pages(login, path, {
limitCode: 'DEPENDENCY_LIMIT',
limitHint: `${path} 的${kind}關係太多,讀不完整份清單`,
})) {
for (const issue of issues) indexes.push(issue.number);
}
return indexes;
}
/**
* 這顆議題上是不是有碼錶在跑。
*
* Gitea 只讓人讀自己的碼錶(`/user/stopwatches`),所以這個欄位的真正語意是
* 「**我**的碼錶正跑在這顆議題上」。它用來提醒自己忘了停錶,不是用來判斷別人有沒有在做
* ——領取鎖看的是 assignee。
*/
async function hasRunningStopwatch(login, repo, index) {
return stopwatchOnIssue(await listStopwatches(login), repo, index) !== null;
}