Files
tea-sdlc/scripts/wp-extract.js
T
jiantw83andClaude Opus 5 feaf40ab78 feat(timer): 碼錶移出領取,等工作樹建成之後才起
原本的順序是「放行 → 設 assignee 與標籤 → 起錶 → 處理分支」,而工作樹建立
失敗會中止整個領取——錶已經起了才失敗,使用者會被計一段什麼都沒做的時間,
而工時要準正是工時報表的立足點。

claim 只留領取鎖的兩件事(assignee 與標籤),起錶交給新的 timer.js,由流程
正本排在 branch-prep 之後。timer 已經跑在這顆議題上時什麼都不做:中斷後重跑
是它最常見的處境,重新起錶會把已經累積的時間切成兩段;跑在別顆上則照舊擋下,
不代勞停錶。

三支腳本讀碼錶的那段各留一份,趁這次收進 lib。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:54:27 +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,
referencedIndex,
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 });
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,
需求議題: referencedIndex(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),
};
});
/**
* 讀一種相依關係上的議題編號。
* 逐頁讀完:半份清單會讓下游把實作順序排錯,那比直接報錯更難發現。
* @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;
}