wp-extract 與 wp-list 都在問「這顆工作包掛在哪顆需求底下」。規則寫兩份, 某天只會有一邊被改到,而分岔的樣子是「清單裡看得到、抽取卻說不是」。 順手把測試裡兩種取段落的寫法統一,並刪掉沒有人傳過的參數。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
123 lines
4.1 KiB
JavaScript
123 lines
4.1 KiB
JavaScript
#!/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;
|
||
}
|