scrum-skills 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/scrum-planning/references/schema.md +3 -2
- package/README.md +17 -8
- package/assets/board/200.html +1 -1
- package/assets/board/404.html +1 -1
- package/assets/board/_nuxt/{C9Eg7sGC.js → -8mcJ5uq.js} +1 -1
- package/assets/board/_nuxt/{DA0S9GHd.js → 9qnowzZi.js} +1 -1
- package/assets/board/_nuxt/{BQSM1Ln0.js → BPTrRhcw.js} +1 -1
- package/assets/board/_nuxt/{B7foC6AH.js → BSov4ehT.js} +1 -1
- package/assets/board/_nuxt/{D8NAsEfE.js → Bb3Limos.js} +1 -1
- package/assets/board/_nuxt/Bi9Axj3V.js +1 -0
- package/assets/board/_nuxt/{C9lP5soO.js → BkjUg7GC.js} +1 -1
- package/assets/board/_nuxt/Bu400pfa.js +1 -0
- package/assets/board/_nuxt/{DiYBussm.js → BuuAjxsV.js} +1 -1
- package/assets/board/_nuxt/{DEBhVwtl.js → C9StZL3D.js} +1 -1
- package/assets/board/_nuxt/{B5u1XkK4.js → CBhhj6QT.js} +1 -1
- package/assets/board/_nuxt/{ADHYAojV.js → CD0uJuyA.js} +1 -1
- package/assets/board/_nuxt/{C3ssq3NT.js → CHZ_IHOa.js} +1 -1
- package/assets/board/_nuxt/{DVC07Kf9.js → CUnX_QKi.js} +1 -1
- package/assets/board/_nuxt/{BJ0EVa07.js → CdvimHoq.js} +1 -1
- package/assets/board/_nuxt/{DhkiugsE.js → ChJuu-Ky.js} +1 -1
- package/assets/board/_nuxt/{DwNOEVN_.js → CjRYYbsC.js} +1 -1
- package/assets/board/_nuxt/Co0ISufR.js +95 -0
- package/assets/board/_nuxt/CuJUHLl8.js +1 -0
- package/assets/board/_nuxt/{BKgQBiDB.js → CvFE1CeN.js} +1 -1
- package/assets/board/_nuxt/{CAJYxF-h.js → CyAQx5wv.js} +1 -1
- package/assets/board/_nuxt/{CviJuCa4.js → D3YCAmfu.js} +1 -1
- package/assets/board/_nuxt/{joIhBaO-.js → D8Bpf7sE.js} +1 -1
- package/assets/board/_nuxt/DDA06Sqv.js +1 -0
- package/assets/board/_nuxt/DFuirznt.js +1 -0
- package/assets/board/_nuxt/{4FVXNzN-.js → DG3IySrP.js} +1 -1
- package/assets/board/_nuxt/{BXQZAVYz.js → DLkAKotw.js} +1 -1
- package/assets/board/_nuxt/DNMF9lr0.js +1 -0
- package/assets/board/_nuxt/{DQRn5Z3H.js → DaqxYsbV.js} +1 -1
- package/assets/board/_nuxt/{jCMbKsJ3.js → DhhR1iR4.js} +1 -1
- package/assets/board/_nuxt/{CL3OrPP7.js → DmKZi-Wo.js} +1 -1
- package/assets/board/_nuxt/{DcWIG384.js → DsNkyZPc.js} +1 -1
- package/assets/board/_nuxt/{BCXs9rBJ.js → DwcwzwSx.js} +1 -1
- package/assets/board/_nuxt/Dzd1U0TD.js +1 -0
- package/assets/board/_nuxt/{BuVRe79A.js → HrhQecsY.js} +1 -1
- package/assets/board/_nuxt/{Db4IStii.js → KhBLkIKl.js} +1 -1
- package/assets/board/_nuxt/{bXQUY80V.js → MTucdeiW.js} +1 -1
- package/assets/board/_nuxt/{CZPwmwA5.js → RJtjdZeG.js} +1 -1
- package/assets/board/_nuxt/Vo4wMbnS.js +1 -0
- package/assets/board/_nuxt/Y5b2rXjB.js +1 -0
- package/assets/board/_nuxt/builds/latest.json +1 -1
- package/assets/board/_nuxt/builds/meta/a2ba436a-230e-43f9-884e-0a5d37f5ba22.json +1 -0
- package/assets/board/_nuxt/{CPfeOlrH.js → d-5IhxZm.js} +1 -1
- package/assets/board/_nuxt/entry.CTKz4ouw.css +2 -0
- package/assets/board/_nuxt/gRdnZpJw.js +1 -0
- package/assets/board/_nuxt/{-_kK9OzO.js → l05SqxhY.js} +1 -1
- package/assets/board/_nuxt/milestones.z-UuT44X.css +1 -0
- package/assets/board/_nuxt/{GzmVZqnA.js → oIa7syc0.js} +1 -1
- package/assets/board/_nuxt/pages.YYGvroeE.css +1 -0
- package/assets/board/_nuxt/{BannxgAO.js → qDn8FnYE.js} +1 -1
- package/assets/board/_nuxt/uGmbQYdY.js +1 -0
- package/assets/board/_nuxt/z9IMa9Iy.js +13 -0
- package/assets/board/burndown/index.html +1 -1
- package/assets/board/epics/index.html +1 -1
- package/assets/board/gantt/index.html +1 -1
- package/assets/board/index.html +1 -1
- package/assets/board/milestones/index.html +1 -1
- package/bin/scrum-skills.mjs +33 -10
- package/package.json +1 -1
- package/templates/AGENTS.md +1 -1
- package/tools/ai.mjs +24 -2
- package/tools/projects.mjs +46 -11
- package/tools/serve.mjs +1 -1
- package/tools/validate.mjs +21 -7
- package/assets/board/_nuxt/BpR5gMZX.js +0 -95
- package/assets/board/_nuxt/BvaAl29K.js +0 -13
- package/assets/board/_nuxt/CDL4GvLz.js +0 -1
- package/assets/board/_nuxt/CTOFhZkc.js +0 -1
- package/assets/board/_nuxt/CZc2Mypg.js +0 -1
- package/assets/board/_nuxt/Cw7Z2eHM.js +0 -1
- package/assets/board/_nuxt/DWxQkuvT.js +0 -1
- package/assets/board/_nuxt/Dpzighda.js +0 -1
- package/assets/board/_nuxt/DttMo5b5.js +0 -1
- package/assets/board/_nuxt/FCuSfyjP.js +0 -1
- package/assets/board/_nuxt/builds/meta/24603984-d514-4dff-badc-898a1fd5e770.json +0 -1
- package/assets/board/_nuxt/entry.ChOvfapw.css +0 -2
- package/assets/board/_nuxt/milestones.CVvAMKg4.css +0 -1
- package/assets/board/_nuxt/pages.DSSttn9g.css +0 -1
package/tools/ai.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* 边收 SSE 边把 text / reasoning / tool 事件吐给前端,浏览器不必碰 Key 也不受 CORS 限制。
|
|
9
9
|
* 3. 工具循环:模型要 search_agile / update_agile 就执行,结果回灌再来一轮;
|
|
10
10
|
* 到轮数上限强制收口(最后一轮不带工具),否则模型会一直调下去。
|
|
11
|
+
* 4. 多模态:前端可以把图片按 OpenAI 的 image_url 一起塞进 user content 数组,
|
|
12
|
+
* 这里原样透传;图片没有文字可估,按 IMAGE_TOKEN_ESTIMATE 计入上下文预算。
|
|
11
13
|
*
|
|
12
14
|
* 审核策略(reviewMode)在这里落地:manual 只调 tools.propose 产待审变更,
|
|
13
15
|
* auto 直接 tools.patch 落盘(写入方自己带校验与回滚)。
|
|
@@ -42,6 +44,8 @@ export const BASE_SYSTEM = [
|
|
|
42
44
|
'2. 不要凭空编 id、状态、日期、人名,也不要造测试数据。',
|
|
43
45
|
'3. 改数据前用一句话说明要改什么,改完说明改了哪几条。',
|
|
44
46
|
'4. 用简体中文,直接给结论:不复述问题、不做自我介绍、不寒暄。',
|
|
47
|
+
'5. 故事状态只有 draft / ready / in-progress / review / done / cancelled(已取消)这六种:'
|
|
48
|
+
+ 'cancelled 是终态,表示这事不做了,要在回答里说清为什么取消;不做了的故事就标 cancelled,不要塞进 done 充数。',
|
|
45
49
|
].join('\n');
|
|
46
50
|
|
|
47
51
|
export const DEFAULT_CONFIG = {
|
|
@@ -123,7 +127,21 @@ export function estimateTokens(text) {
|
|
|
123
127
|
return cjk * 0.7 + rest / 4;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
|
|
130
|
+
/**
|
|
131
|
+
* 一张图在请求里没有文字可估,按固定额度算。
|
|
132
|
+
* 不估(当成 0)会让「塞一堆截图」的对话永远不触发裁剪,请求体越滚越大直到接口 413;
|
|
133
|
+
* 估得偏高则是安全的:多裁几轮老对话,总比请求发不出去强。
|
|
134
|
+
*/
|
|
135
|
+
export const IMAGE_TOKEN_ESTIMATE = 800;
|
|
136
|
+
|
|
137
|
+
const contentCost = content => (Array.isArray(content)
|
|
138
|
+
? content.reduce(
|
|
139
|
+
(sum, part) => sum + (part?.type === 'image_url' ? IMAGE_TOKEN_ESTIMATE : estimateTokens(part?.text ?? '')),
|
|
140
|
+
0,
|
|
141
|
+
)
|
|
142
|
+
: estimateTokens(content));
|
|
143
|
+
|
|
144
|
+
const costOf = messages => messages.reduce((sum, m) => sum + contentCost(m.content) + 8, 0);
|
|
127
145
|
|
|
128
146
|
/**
|
|
129
147
|
* 上下文预算:保留系统提示词与最后一块(当前提问),从最早处整块丢弃。
|
|
@@ -328,7 +346,11 @@ export function createAiService({ root, store = defaultStore, token = '', tools,
|
|
|
328
346
|
});
|
|
329
347
|
if (!response.ok) {
|
|
330
348
|
const detail = await response.text().catch(() => '');
|
|
331
|
-
|
|
349
|
+
// 带图被拒最常见的原因是模型不是多模态:DeepSeek 的 chat / reasoner 都不收 image_url。
|
|
350
|
+
// 不点破的话用户只看到一行 400,会以为是 Key 或网络问题。
|
|
351
|
+
const withImages = convo.some(m => Array.isArray(m.content) && m.content.some(p => p?.type === 'image_url'));
|
|
352
|
+
const hint = withImages ? '(这一轮带了图片:当前模型可能不支持图片输入,请在设置里换成支持视觉模型的接口,或去掉图片再发)' : '';
|
|
353
|
+
yield { type: 'error', message: `模型接口返回 ${response.status}:${detail.slice(0, 300) || response.statusText}${hint}` };
|
|
332
354
|
return;
|
|
333
355
|
}
|
|
334
356
|
|
package/tools/projects.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { execFile, execFileSync } from 'node:child_process';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { randomBytes } from 'node:crypto';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { createAgileTools } from './agile-tools.mjs';
|
|
8
9
|
|
|
9
10
|
const exec = promisify(execFile);
|
|
10
11
|
const validator = fileURLToPath(new URL('./validate.mjs', import.meta.url));
|
|
@@ -58,6 +59,24 @@ try {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 故事状态单向车道。看板卡片的「下一步 / 退回」就按这条线走,
|
|
64
|
+
* done 是终点、cancelled 是另一条终点,两者都不许再动。
|
|
65
|
+
*/
|
|
66
|
+
export const STORY_FLOW = ['draft', 'ready', 'in-progress', 'review', 'done'];
|
|
67
|
+
|
|
68
|
+
const ACTION_LABEL = { next: '推进到下一状态', prev: '退回上一状态', cancel: '标记为已取消' };
|
|
69
|
+
|
|
70
|
+
/** 按动作算出目标状态;算不出来(已是终点 / 动作不认识)返回 null */
|
|
71
|
+
export function statusAfter(from, action) {
|
|
72
|
+
if (action === 'cancel') return ['draft', 'ready', 'in-progress', 'review'].includes(from) ? 'cancelled' : null;
|
|
73
|
+
if (!STORY_FLOW.includes(from)) return null;
|
|
74
|
+
const i = STORY_FLOW.indexOf(from);
|
|
75
|
+
if (action === 'next') return STORY_FLOW[i + 1] ?? null;
|
|
76
|
+
if (action === 'prev') return STORY_FLOW[i - 1] ?? null;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
function snapshot(root) {
|
|
62
81
|
const read = file => JSON.parse(readFileSync(join(root, 'agile', file + '.json'), 'utf8'));
|
|
63
82
|
return { backlog: read('backlog'), sprints: read('sprints'), journal: read('journal'), meta: { root, servedAt: new Date().toISOString() } };
|
|
@@ -78,18 +97,12 @@ export function createProjects({ root, store = defaultStore, pick = pickDirector
|
|
|
78
97
|
const saved = JSON.parse(readFileSync(store, 'utf8'));
|
|
79
98
|
if (Array.isArray(saved.recent)) recent = saved.recent.filter(x => typeof x === 'string').slice(0, 10);
|
|
80
99
|
} catch {}
|
|
100
|
+
// 启动目录优先:从哪个目录起服务,看板就打开哪个目录。
|
|
101
|
+
// 最近列表只是「切回上次那个项目」的入口,不许顶替启动目录——
|
|
102
|
+
// 否则在 A 项目里 serve,会悄悄打开上一次的 B 项目数据(JSON 看着像是 A 的,其实全错)。
|
|
81
103
|
const initial = realpathSync(root);
|
|
82
104
|
let current = initial;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const path = realpathSync(candidate);
|
|
86
|
-
validate(path);
|
|
87
|
-
snapshot(path);
|
|
88
|
-
current = path;
|
|
89
|
-
break;
|
|
90
|
-
} catch {}
|
|
91
|
-
}
|
|
92
|
-
recent = [...new Set([current, ...recent, initial])].slice(0, 10);
|
|
105
|
+
recent = [...new Set([current, ...recent])].slice(0, 10);
|
|
93
106
|
let selecting = false;
|
|
94
107
|
const list = () => ({ recent: [...recent], root: current, token });
|
|
95
108
|
const read = (path = current) => {
|
|
@@ -115,7 +128,25 @@ export function createProjects({ root, store = defaultStore, pick = pickDirector
|
|
|
115
128
|
return selected ? await open(selected) : null;
|
|
116
129
|
} finally { selecting = false; }
|
|
117
130
|
};
|
|
118
|
-
|
|
131
|
+
// 写盘复用 AI 助手那套「文本补丁 + 写后校验 + 失败回滚」,
|
|
132
|
+
// 不另写一份:改一个状态只差一行字符,校验不过自动还原。
|
|
133
|
+
const tools = createAgileTools({ root: () => current });
|
|
134
|
+
const setStoryStatus = ({ id, action } = {}) => {
|
|
135
|
+
if (!id) throw new Error('缺少故事 id');
|
|
136
|
+
const { backlog } = snapshot(current);
|
|
137
|
+
const story = (backlog.stories ?? []).find(s => s.id === id);
|
|
138
|
+
if (!story) throw new Error(`找不到故事 ${id}`);
|
|
139
|
+
const from = story.status;
|
|
140
|
+
const to = statusAfter(from, action);
|
|
141
|
+
if (!to) throw new Error(`${id} 当前是 ${from},没法${ACTION_LABEL[action] ?? `执行 ${action}`}`);
|
|
142
|
+
const patches = [{ path: `stories.${id}.status`, value: to }];
|
|
143
|
+
// 取消一条还被阻塞的故事:阻塞标记要一起摘掉。
|
|
144
|
+
// 校验器不许「已取消却还标着 blocked」,不摘会整条改动作废。
|
|
145
|
+
if (to === 'cancelled' && story.blocked) patches.push({ op: 'remove', path: `stories.${id}.blocked` });
|
|
146
|
+
tools.patch({ file: 'backlog', patches, reason: `看板卡片操作:${from} → ${to}` });
|
|
147
|
+
return { id, from, to };
|
|
148
|
+
};
|
|
149
|
+
return { list, read, open, select, setStoryStatus };
|
|
119
150
|
}
|
|
120
151
|
|
|
121
152
|
export async function handleProjectApi(projects, { method, path, headers, body }) {
|
|
@@ -130,6 +161,10 @@ export async function handleProjectApi(projects, { method, path, headers, body }
|
|
|
130
161
|
if (method === 'GET' && url.pathname === '/api/agile') return { status: 200, body: projects.read(url.searchParams.get('root') || undefined) };
|
|
131
162
|
if (method !== 'POST') return { status: 405, body: { error: '不支持的请求方法。' } };
|
|
132
163
|
if (headers['x-project-token'] !== projects.list().token) return denied();
|
|
164
|
+
// 看板卡片的退回 / 下一步 / 取消:改的是当前打开项目的 backlog.json
|
|
165
|
+
if (url.pathname === '/api/agile/story-status') {
|
|
166
|
+
return { status: 200, body: projects.setStoryStatus({ id: body?.id, action: body?.action }) };
|
|
167
|
+
}
|
|
133
168
|
if (url.pathname === '/api/projects/select') return { status: 200, body: await projects.select() };
|
|
134
169
|
if (url.pathname === '/api/projects/open') {
|
|
135
170
|
if (!projects.list().recent.includes(body?.root)) throw new Error('该项目不在最近列表中,请选择文件夹。');
|
package/tools/serve.mjs
CHANGED
|
@@ -112,7 +112,7 @@ const server = createServer(async (req, res) => {
|
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
if (pathname
|
|
115
|
+
if (pathname.startsWith('/api/agile') || pathname.startsWith('/api/projects')) {
|
|
116
116
|
try {
|
|
117
117
|
let text = '';
|
|
118
118
|
for await (const chunk of req) {
|
package/tools/validate.mjs
CHANGED
|
@@ -50,7 +50,9 @@ const sprints = Array.isArray(sprintsFile.sprints) ? sprintsFile.sprints : [];
|
|
|
50
50
|
const milestones = Array.isArray(sprintsFile.milestones) ? sprintsFile.milestones : [];
|
|
51
51
|
const journals = Array.isArray(journalFile.journals) ? journalFile.journals : [];
|
|
52
52
|
|
|
53
|
-
const STORY_FLOW = ['draft', 'ready', 'in-progress', 'review', 'done'];
|
|
53
|
+
const STORY_FLOW = ['draft', 'ready', 'in-progress', 'review', 'done', 'cancelled'];
|
|
54
|
+
// 终态:done 与 cancelled 都不再往前走,也不再要求验收 / 任务全部做完
|
|
55
|
+
const STORY_TERMINAL = ['done', 'cancelled'];
|
|
54
56
|
const TASK_FLOW = ['todo', 'red', 'green', 'refactored', 'done'];
|
|
55
57
|
const EPIC_FLOW = ['draft', 'active', 'done', 'cancelled'];
|
|
56
58
|
const MILESTONE_FLOW = ['planned', 'at-risk', 'reached', 'missed'];
|
|
@@ -136,7 +138,8 @@ for (const s of stories) {
|
|
|
136
138
|
else if (!(epicById.get(epicId).stories ?? []).includes(id))
|
|
137
139
|
err(`story ${id}: 挂在 ${epicId} 上,但该 epic 的 stories 里没有它(双向引用不一致)`);
|
|
138
140
|
|
|
139
|
-
if (!STORY_FLOW.includes(status)
|
|
141
|
+
if (!STORY_FLOW.includes(status))
|
|
142
|
+
err(`story ${id}: 非法状态 ${status}(合法:${STORY_FLOW.join('/')})`);
|
|
140
143
|
if (!Number.isFinite(points)) err(`story ${id}: points 必须是数字`);
|
|
141
144
|
else if (![1, 2, 3, 5, 8, 13].includes(points)) warn(`story ${id}: points ${points} 不在斐波那契序列`);
|
|
142
145
|
|
|
@@ -153,7 +156,8 @@ for (const s of stories) {
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
// 取消掉的故事不会再交付,别逼着为它补验收标准
|
|
160
|
+
if (['ready', 'in-progress', 'review', 'done'].includes(status) && (!acceptance || acceptance.length === 0))
|
|
157
161
|
err(`story ${id}: 状态 ${status} 但验收标准为空`);
|
|
158
162
|
for (const [i, a] of (acceptance ?? []).entries()) {
|
|
159
163
|
if (!a.given || !a.when || !a.then) err(`story ${id}: 第 ${i + 1} 条验收标准缺 given/when/then`);
|
|
@@ -163,7 +167,8 @@ for (const s of stories) {
|
|
|
163
167
|
if (!hit) err(`story ${id}: 第 ${i + 1} 条验收的 verifyBy ${tid} 不存在`);
|
|
164
168
|
else if (hit.story.id !== id) err(`story ${id}: 第 ${i + 1} 条验收的 verifyBy ${tid} 属于 ${hit.story.id}`);
|
|
165
169
|
}
|
|
166
|
-
if (!(a.verifyBy ?? []).length &&
|
|
170
|
+
if (!(a.verifyBy ?? []).length && !['draft', 'cancelled'].includes(status))
|
|
171
|
+
warn(`story ${id}: 第 ${i + 1} 条验收没有 verifyBy(没绑定验证任务)`);
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
// 阻塞 / 依赖
|
|
@@ -175,7 +180,9 @@ for (const s of stories) {
|
|
|
175
180
|
if (!s.blocked.reason) err(`story ${id}: blocked 缺少 reason(堵在哪要说清)`);
|
|
176
181
|
if (!DATE_RE.test(s.blocked.since ?? '')) err(`story ${id}: blocked.since 格式应为 YYYY-MM-DD`);
|
|
177
182
|
if (status === 'done') err(`story ${id}: 已 done 却还标着 blocked`);
|
|
178
|
-
if (
|
|
183
|
+
if (status === 'cancelled') err(`story ${id}: 已取消却还标着 blocked(不做了就别再挂着阻塞)`);
|
|
184
|
+
if (sprintId && !STORY_TERMINAL.includes(status))
|
|
185
|
+
warn(`story ${id}: 处于 ${sprintId} 但被阻塞(${s.blocked.reason}),站会必须处理`);
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
// 范围变更标记
|
|
@@ -191,7 +198,8 @@ for (const s of stories) {
|
|
|
191
198
|
if (!sp) err(`story ${id}: sprintId ${sprintId} 不存在`);
|
|
192
199
|
else if (!(sp.stories ?? []).includes(id))
|
|
193
200
|
err(`story ${id}: 指向 ${sprintId},但该 sprint 的 stories 里没有它(双向引用不一致)`);
|
|
194
|
-
if (!acceptance || acceptance.length === 0)
|
|
201
|
+
if (status !== 'cancelled' && (!acceptance || acceptance.length === 0))
|
|
202
|
+
err(`story ${id}: 已进 sprint 但无验收标准(不满足 ready)`);
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
if (s.schedule) {
|
|
@@ -230,8 +238,14 @@ for (const sp of sprints) {
|
|
|
230
238
|
const st = storyById.get(sid);
|
|
231
239
|
if (!st) { err(`sprint ${sp.id}: 引用 story ${sid} 不存在`); continue; }
|
|
232
240
|
if (st.sprintId !== sp.id) err(`sprint ${sp.id}: story ${sid} 的 sprintId 是 ${st.sprintId},双向引用不一致`);
|
|
241
|
+
if (st.status === 'cancelled')
|
|
242
|
+
warn(`story ${sid}: 已取消但仍排在 ${sp.id} 里,建议从迭代移出(点数已不计入承诺)`);
|
|
233
243
|
}
|
|
234
|
-
|
|
244
|
+
// 已取消的故事不再占迭代容量:承诺点数只算还可能交付的
|
|
245
|
+
const committed = (sp.stories ?? []).reduce((a, sid) => {
|
|
246
|
+
const st = storyById.get(sid);
|
|
247
|
+
return st && st.status !== 'cancelled' ? a + (st.points ?? 0) : a;
|
|
248
|
+
}, 0);
|
|
235
249
|
if (Number.isFinite(sp.capacity) && committed > sp.capacity)
|
|
236
250
|
warn(`sprint ${sp.id}: 承诺 ${committed} 点超过容量 ${sp.capacity} 点`);
|
|
237
251
|
if (sp.start <= today && sp.end >= today) active++;
|