#!/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; }