@xihe-lab/tapd-cli 2.0.0-rc.1 → 2.0.0-rc.2
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/README.md +22 -0
- package/dist/advisor/domain-axis.d.ts +16 -0
- package/dist/advisor/domain-axis.d.ts.map +1 -0
- package/dist/advisor/domain-axis.js +43 -0
- package/dist/advisor/domain-axis.js.map +1 -0
- package/dist/advisor/domain-axis.test.d.ts +2 -0
- package/dist/advisor/domain-axis.test.d.ts.map +1 -0
- package/dist/advisor/domain-axis.test.js +198 -0
- package/dist/advisor/domain-axis.test.js.map +1 -0
- package/dist/advisor/index.d.ts +8 -0
- package/dist/advisor/index.d.ts.map +1 -0
- package/dist/advisor/index.js +6 -0
- package/dist/advisor/index.js.map +1 -0
- package/dist/advisor/persona.d.ts +12 -0
- package/dist/advisor/persona.d.ts.map +1 -0
- package/dist/advisor/persona.js +34 -0
- package/dist/advisor/persona.js.map +1 -0
- package/dist/advisor/recall.d.ts +38 -0
- package/dist/advisor/recall.d.ts.map +1 -0
- package/dist/advisor/recall.js +96 -0
- package/dist/advisor/recall.js.map +1 -0
- package/dist/advisor/render.d.ts +8 -0
- package/dist/advisor/render.d.ts.map +1 -0
- package/dist/advisor/render.js +42 -0
- package/dist/advisor/render.js.map +1 -0
- package/dist/commands/advisor.d.ts.map +1 -1
- package/dist/commands/advisor.js +31 -13
- package/dist/commands/advisor.js.map +1 -1
- package/dist/commands/pipeline.d.ts +4 -0
- package/dist/commands/pipeline.d.ts.map +1 -0
- package/dist/commands/pipeline.js +178 -0
- package/dist/commands/pipeline.js.map +1 -0
- package/dist/commands/pm.d.ts +4 -0
- package/dist/commands/pm.d.ts.map +1 -0
- package/dist/commands/pm.js +175 -0
- package/dist/commands/pm.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/engine.d.ts +38 -0
- package/dist/pipeline/engine.d.ts.map +1 -0
- package/dist/pipeline/engine.js +326 -0
- package/dist/pipeline/engine.js.map +1 -0
- package/dist/pipeline/engine.test.d.ts +2 -0
- package/dist/pipeline/engine.test.d.ts.map +1 -0
- package/dist/pipeline/engine.test.js +419 -0
- package/dist/pipeline/engine.test.js.map +1 -0
- package/dist/pipeline/expression.d.ts +31 -0
- package/dist/pipeline/expression.d.ts.map +1 -0
- package/dist/pipeline/expression.js +249 -0
- package/dist/pipeline/expression.js.map +1 -0
- package/dist/pipeline/expression.test.d.ts +2 -0
- package/dist/pipeline/expression.test.d.ts.map +1 -0
- package/dist/pipeline/expression.test.js +111 -0
- package/dist/pipeline/expression.test.js.map +1 -0
- package/dist/pipeline/history.d.ts +13 -0
- package/dist/pipeline/history.d.ts.map +1 -0
- package/dist/pipeline/history.js +66 -0
- package/dist/pipeline/history.js.map +1 -0
- package/dist/pipeline/index.d.ts +13 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +9 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/record.d.ts +13 -0
- package/dist/pipeline/record.d.ts.map +1 -0
- package/dist/pipeline/record.js +30 -0
- package/dist/pipeline/record.js.map +1 -0
- package/dist/pipeline/record.test.d.ts +2 -0
- package/dist/pipeline/record.test.d.ts.map +1 -0
- package/dist/pipeline/record.test.js +109 -0
- package/dist/pipeline/record.test.js.map +1 -0
- package/dist/pipeline/template.d.ts +24 -0
- package/dist/pipeline/template.d.ts.map +1 -0
- package/dist/pipeline/template.js +134 -0
- package/dist/pipeline/template.js.map +1 -0
- package/dist/pipeline/template.test.d.ts +2 -0
- package/dist/pipeline/template.test.d.ts.map +1 -0
- package/dist/pipeline/template.test.js +94 -0
- package/dist/pipeline/template.test.js.map +1 -0
- package/dist/pipeline/types.d.ts +99 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +31 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/pipeline/unwrap.d.ts +3 -0
- package/dist/pipeline/unwrap.d.ts.map +1 -0
- package/dist/pipeline/unwrap.js +26 -0
- package/dist/pipeline/unwrap.js.map +1 -0
- package/dist/pipeline/unwrap.test.d.ts +2 -0
- package/dist/pipeline/unwrap.test.d.ts.map +1 -0
- package/dist/pipeline/unwrap.test.js +36 -0
- package/dist/pipeline/unwrap.test.js.map +1 -0
- package/dist/pipeline/validate.d.ts +18 -0
- package/dist/pipeline/validate.d.ts.map +1 -0
- package/dist/pipeline/validate.js +134 -0
- package/dist/pipeline/validate.js.map +1 -0
- package/dist/pipeline/validate.test.d.ts +2 -0
- package/dist/pipeline/validate.test.d.ts.map +1 -0
- package/dist/pipeline/validate.test.js +166 -0
- package/dist/pipeline/validate.test.js.map +1 -0
- package/dist/pm/discovery.d.ts +54 -0
- package/dist/pm/discovery.d.ts.map +1 -0
- package/dist/pm/discovery.js +198 -0
- package/dist/pm/discovery.js.map +1 -0
- package/dist/pm/discovery.test.d.ts +2 -0
- package/dist/pm/discovery.test.d.ts.map +1 -0
- package/dist/pm/discovery.test.js +147 -0
- package/dist/pm/discovery.test.js.map +1 -0
- package/dist/pm/domains.d.ts +15 -0
- package/dist/pm/domains.d.ts.map +1 -0
- package/dist/pm/domains.js +27 -0
- package/dist/pm/domains.js.map +1 -0
- package/dist/pm/domains.test.d.ts +2 -0
- package/dist/pm/domains.test.d.ts.map +1 -0
- package/dist/pm/domains.test.js +141 -0
- package/dist/pm/domains.test.js.map +1 -0
- package/dist/pm/index.d.ts +9 -0
- package/dist/pm/index.d.ts.map +1 -0
- package/dist/pm/index.js +6 -0
- package/dist/pm/index.js.map +1 -0
- package/dist/pm/iteration.d.ts +26 -0
- package/dist/pm/iteration.d.ts.map +1 -0
- package/dist/pm/iteration.js +50 -0
- package/dist/pm/iteration.js.map +1 -0
- package/dist/pm/iteration.test.d.ts +2 -0
- package/dist/pm/iteration.test.d.ts.map +1 -0
- package/dist/pm/iteration.test.js +125 -0
- package/dist/pm/iteration.test.js.map +1 -0
- package/dist/pm/pm-command.test.d.ts +2 -0
- package/dist/pm/pm-command.test.d.ts.map +1 -0
- package/dist/pm/pm-command.test.js +321 -0
- package/dist/pm/pm-command.test.js.map +1 -0
- package/dist/pm/render.d.ts +17 -0
- package/dist/pm/render.d.ts.map +1 -0
- package/dist/pm/render.js +82 -0
- package/dist/pm/render.js.map +1 -0
- package/dist/pm/tpl-communication-stakeholder.test.d.ts +2 -0
- package/dist/pm/tpl-communication-stakeholder.test.d.ts.map +1 -0
- package/dist/pm/tpl-communication-stakeholder.test.js +392 -0
- package/dist/pm/tpl-communication-stakeholder.test.js.map +1 -0
- package/dist/pm/tpl-integration-scope.test.d.ts +2 -0
- package/dist/pm/tpl-integration-scope.test.d.ts.map +1 -0
- package/dist/pm/tpl-integration-scope.test.js +387 -0
- package/dist/pm/tpl-integration-scope.test.js.map +1 -0
- package/dist/pm/tpl-quality-risk.test.d.ts +2 -0
- package/dist/pm/tpl-quality-risk.test.d.ts.map +1 -0
- package/dist/pm/tpl-quality-risk.test.js +414 -0
- package/dist/pm/tpl-quality-risk.test.js.map +1 -0
- package/dist/pm/tpl-schedule-cost.test.d.ts +2 -0
- package/dist/pm/tpl-schedule-cost.test.d.ts.map +1 -0
- package/dist/pm/tpl-schedule-cost.test.js +376 -0
- package/dist/pm/tpl-schedule-cost.test.js.map +1 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +6 -0
- package/dist/program.js.map +1 -1
- package/package.json +11 -6
- package/templates/deliver-story.yaml +38 -0
- package/templates/iteration-review.yaml +52 -0
- package/templates/pm/README.md +57 -0
- package/templates/pm/communication-notify.yaml +65 -0
- package/templates/pm/communication-weekly.yaml +160 -0
- package/templates/pm/cost-report.yaml +74 -0
- package/templates/pm/integration-close.yaml +118 -0
- package/templates/pm/integration-health.yaml +155 -0
- package/templates/pm/integration-kickoff.yaml +94 -0
- package/templates/pm/quality-dashboard.yaml +88 -0
- package/templates/pm/quality-triage.yaml +42 -0
- package/templates/pm/risk-register.yaml +55 -0
- package/templates/pm/risk-scan.yaml +87 -0
- package/templates/pm/schedule-burndown.yaml +87 -0
- package/templates/pm/schedule-standup.yaml +109 -0
- package/templates/pm/scope-baseline.yaml +93 -0
- package/templates/pm/scope-drift-check.yaml +129 -0
- package/templates/pm/stakeholder-map.yaml +121 -0
- package/templates/triage-bugs.yaml +49 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# 项目启动骨架(写场景:默认 dry-run 闸门预览,--yes 确认后依次落库)—— 整合管理(软考高项第4版 第8章)
|
|
2
|
+
# 场景: tapd pm integration kickoff
|
|
3
|
+
# --input '{"project_name":"XX系统","startdate":"2026-09-11","enddate":"2026-10-11","wiki_content":"# 章程正文(md)"}' --yes
|
|
4
|
+
# 编排(3 个写步骤串行): tapd_create_wiki(章程) → tapd_create_iteration(首迭代)
|
|
5
|
+
# → tapd_create_release(发布计划;TAPD 发布计划无原生迭代关联字段,
|
|
6
|
+
# 依赖前步产出的迭代 ID 经 ${steps...} 记录进 description)
|
|
7
|
+
# 本地 expr 步骤只做创建结果形态归一(Iteration/Wiki/Release 包裹兼容),不触 API
|
|
8
|
+
name: integration-kickoff
|
|
9
|
+
domain: integration
|
|
10
|
+
title: 项目启动骨架(章程 wiki + 首迭代 + 发布计划三步落库,软考第8章)
|
|
11
|
+
write: true
|
|
12
|
+
unwrap: true
|
|
13
|
+
vars:
|
|
14
|
+
project_name: "未命名项目"
|
|
15
|
+
startdate: ""
|
|
16
|
+
enddate: ""
|
|
17
|
+
wiki_content: ""
|
|
18
|
+
steps:
|
|
19
|
+
# 写步骤 1/3:项目章程 wiki(markdown 正文)
|
|
20
|
+
- id: charter
|
|
21
|
+
uses: tapd_create_wiki
|
|
22
|
+
args:
|
|
23
|
+
name: "${vars.project_name} 项目章程"
|
|
24
|
+
markdown_description: "${vars.wiki_content}"
|
|
25
|
+
note: "由 tapd pm integration kickoff 生成;项目周期 ${vars.startdate} ~ ${vars.enddate}"
|
|
26
|
+
|
|
27
|
+
- id: charter_ref
|
|
28
|
+
expr: >-
|
|
29
|
+
(() => {
|
|
30
|
+
const w = steps.charter.data || {};
|
|
31
|
+
const inner = w.Wiki || w.wiki || w;
|
|
32
|
+
return { id: String(inner.id || ''), title: inner.name || '' };
|
|
33
|
+
})()
|
|
34
|
+
depends_on: [charter]
|
|
35
|
+
|
|
36
|
+
# 写步骤 2/3:首迭代(open)
|
|
37
|
+
- id: new_iteration
|
|
38
|
+
uses: tapd_create_iteration
|
|
39
|
+
args:
|
|
40
|
+
name: "${vars.project_name} 首迭代"
|
|
41
|
+
startdate: "${vars.startdate}"
|
|
42
|
+
enddate: "${vars.enddate}"
|
|
43
|
+
status: open
|
|
44
|
+
description: "项目启动骨架创建;章程 wiki#${steps.charter_ref.data.id} ${steps.charter_ref.data.title}"
|
|
45
|
+
depends_on: [charter_ref]
|
|
46
|
+
|
|
47
|
+
- id: iteration_ref
|
|
48
|
+
expr: >-
|
|
49
|
+
(() => {
|
|
50
|
+
const r = steps.new_iteration.data || {};
|
|
51
|
+
const inner = r.Iteration || r.iteration || r;
|
|
52
|
+
return { id: String(inner.id || ''), name: inner.name || '' };
|
|
53
|
+
})()
|
|
54
|
+
depends_on: [new_iteration]
|
|
55
|
+
|
|
56
|
+
# 写步骤 3/3:发布计划(迭代 ID 以描述记录:tapd_create_release 无迭代关联字段)
|
|
57
|
+
- id: release
|
|
58
|
+
uses: tapd_create_release
|
|
59
|
+
args:
|
|
60
|
+
name: "${vars.project_name} 首发布计划"
|
|
61
|
+
start_date: "${vars.startdate}"
|
|
62
|
+
end_date: "${vars.enddate}"
|
|
63
|
+
release_date: "${vars.enddate}"
|
|
64
|
+
description: "由 pm integration kickoff 创建;关联首迭代 ${steps.iteration_ref.data.name}(id=${steps.iteration_ref.data.id})"
|
|
65
|
+
depends_on: [iteration_ref]
|
|
66
|
+
|
|
67
|
+
- id: summary
|
|
68
|
+
expr: >-
|
|
69
|
+
(() => {
|
|
70
|
+
const r = steps.release.data || {};
|
|
71
|
+
const inner = r.Release || r.release || r;
|
|
72
|
+
return {
|
|
73
|
+
range: (vars.startdate || '?') + ' ~ ' + (vars.enddate || '?'),
|
|
74
|
+
wikiId: steps.charter_ref.data.id,
|
|
75
|
+
wikiTitle: steps.charter_ref.data.title,
|
|
76
|
+
iterationId: steps.iteration_ref.data.id,
|
|
77
|
+
iterationName: steps.iteration_ref.data.name,
|
|
78
|
+
releaseId: String(inner.id || ''),
|
|
79
|
+
};
|
|
80
|
+
})()
|
|
81
|
+
depends_on: [release]
|
|
82
|
+
|
|
83
|
+
report:
|
|
84
|
+
format: markdown
|
|
85
|
+
template: |
|
|
86
|
+
# 项目启动骨架已创建 — ${vars.project_name}
|
|
87
|
+
|
|
88
|
+
- 项目周期: ${steps.summary.data.range}
|
|
89
|
+
- 章程 wiki: ${steps.summary.data.wikiTitle}(wiki#${steps.summary.data.wikiId})
|
|
90
|
+
- 首迭代: ${steps.summary.data.iterationName}(id=${steps.summary.data.iterationId})
|
|
91
|
+
- 发布计划: ${steps.summary.data.releaseId}
|
|
92
|
+
|
|
93
|
+
> 本场景含 3 个写步骤(wiki create → iteration create → release create,串行依赖)。
|
|
94
|
+
> 不加 --yes 时仅输出计划预览不落库;后续用 `tapd pm scope baseline` 冻结范围基线。
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 质量度量盘(S5 / 软考高项第12章 质量管理)
|
|
2
|
+
# 只读场景(L0 直接执行):缺陷 severity×status 分布 + 未解清单 + 缺陷密度(保留分母)
|
|
3
|
+
# 可选测试计划进度:pm 默认拿不到 test_plan_id,需经 --input '{"test_plan_id":"..."}' 传入;
|
|
4
|
+
# 未传时跳过该步骤并在报告中注明(不报错)。
|
|
5
|
+
# 用法: tapd pm quality dashboard --iteration <id>
|
|
6
|
+
# 或: tapd pm quality dashboard --input '{"test_plan_id":"1139814312001001234"}'
|
|
7
|
+
name: quality-dashboard
|
|
8
|
+
domain: quality
|
|
9
|
+
title: 质量度量盘(缺陷分布/未解清单/缺陷密度,可选测试计划进度)
|
|
10
|
+
write: false
|
|
11
|
+
unwrap: true
|
|
12
|
+
vars:
|
|
13
|
+
iteration_id: ""
|
|
14
|
+
test_plan_id: ""
|
|
15
|
+
steps:
|
|
16
|
+
- id: iteration
|
|
17
|
+
uses: tapd_get_iterations
|
|
18
|
+
args:
|
|
19
|
+
id: "${vars.iteration_id}"
|
|
20
|
+
fields: "id,name,startdate,enddate,status"
|
|
21
|
+
where: "String(vars.iteration_id || '').trim() !== ''"
|
|
22
|
+
|
|
23
|
+
- id: bugs
|
|
24
|
+
uses: tapd_get_bugs
|
|
25
|
+
args:
|
|
26
|
+
iteration_id: "${vars.iteration_id}"
|
|
27
|
+
limit: 200
|
|
28
|
+
fields: "id,title,severity,status,priority_label,current_owner,te,de,deadline"
|
|
29
|
+
filter: "!item.title.startsWith('zzz-delete-me')"
|
|
30
|
+
|
|
31
|
+
# 范围分母:剔除 zzz 脏数据与【风险】项(label=risk / 名称【风险】前缀,登记后不再计入范围度量)
|
|
32
|
+
- id: stories
|
|
33
|
+
uses: tapd_get_stories
|
|
34
|
+
args:
|
|
35
|
+
iteration_id: "${vars.iteration_id}"
|
|
36
|
+
limit: 200
|
|
37
|
+
fields: "id,name,status,owner"
|
|
38
|
+
filter: "!item.name.startsWith('zzz-delete-me') && !item.name.startsWith('【风险】')"
|
|
39
|
+
|
|
40
|
+
- id: test_plan
|
|
41
|
+
uses: tapd_get_test_plan_progress
|
|
42
|
+
args:
|
|
43
|
+
test_plan_id: "${vars.test_plan_id}"
|
|
44
|
+
where: "String(vars.test_plan_id || '').trim() !== ''"
|
|
45
|
+
|
|
46
|
+
- id: open_bugs
|
|
47
|
+
expr: "(() => { const asArr = v => Array.isArray(v) ? v : []; const norm = s => String(s === undefined || s === null ? '' : s).trim().toLowerCase(); const UNRESOLVED = ['closed', 'resolved', 'rejected', 'reject']; return asArr(steps.bugs.data).filter(b => !UNRESOLVED.includes(norm(b.status))); })()"
|
|
48
|
+
depends_on: [bugs]
|
|
49
|
+
|
|
50
|
+
- id: distribution
|
|
51
|
+
expr: "(() => { const asArr = v => Array.isArray(v) ? v : []; const norm = v => { const s = String(v === undefined || v === null ? '' : v).trim(); return s === '' ? '-' : s; }; const bugs = asArr(steps.bugs.data); const sevs = [...new Set(bugs.map(b => norm(b.severity)))].sort((a, b) => (Number(a) || 99) - (Number(b) || 99)); const stats = [...new Set(bugs.map(b => norm(b.status)))].sort(); const cell = (sv, st) => bugs.filter(b => norm(b.severity) === sv && norm(b.status) === st).length; const head = '| 严重度 | ' + stats.join(' | ') + ' | 合计 |'; const sep = '| ' + Array(stats.length + 2).fill('---').join(' | ') + ' |'; const rows = sevs.map(sv => '| ' + sv + ' | ' + stats.map(st => cell(sv, st)).join(' | ') + ' | ' + bugs.filter(b => norm(b.severity) === sv).length + ' |'); const bySeverity = {}; const byStatus = {}; for (const b of bugs) { bySeverity[norm(b.severity)] = (bySeverity[norm(b.severity)] || 0) + 1; byStatus[norm(b.status)] = (byStatus[norm(b.status)] || 0) + 1; } return { total: bugs.length, sevs, stats, bySeverity, byStatus, matrixTable: [head, sep].concat(rows).join('\\n') }; })()"
|
|
52
|
+
depends_on: [bugs, open_bugs]
|
|
53
|
+
|
|
54
|
+
- id: stats
|
|
55
|
+
expr: "(() => { const total = steps.distribution.data.total; const open = steps.open_bugs.data.length; const stories = steps.stories.data.length; return { total, open, stories, densityText: stories > 0 ? (total + '/' + stories) : (total + '/0(需求分母为 0,密度无意义)'), densityNum: stories > 0 ? Math.round((total / stories) * 100) / 100 : null }; })()"
|
|
56
|
+
depends_on: [distribution, stories]
|
|
57
|
+
|
|
58
|
+
# 注意:依赖声明用 bugs(wave 0)而非 iteration —— iteration 可能被 where 跳过,
|
|
59
|
+
# 依赖被跳过步骤会连锁跳过;挂到下一波次可确定性读取 steps.iteration 的最终状态。
|
|
60
|
+
- id: header
|
|
61
|
+
expr: "(() => { const list = steps.iteration && Array.isArray(steps.iteration.data) ? steps.iteration.data : []; const it = list[0]; return { name: it ? it.name : '(未识别迭代,请检查 --iteration)', range: it ? ((it.startdate || '-') + ' ~ ' + (it.enddate || '-')) : '-' }; })()"
|
|
62
|
+
depends_on: [bugs]
|
|
63
|
+
|
|
64
|
+
# 注意:test_plan 可能被 where 跳过(未传 test_plan_id),直接依赖它会连锁跳过;
|
|
65
|
+
# 挂到 stats(必然后于 wave 0 的 test_plan)之后,可确定性读取其执行/跳过状态。
|
|
66
|
+
- id: testplan_note
|
|
67
|
+
expr: "(() => { const tp = steps.test_plan; if (!tp || tp.data === undefined || tp.data === null) return '未提供 vars.test_plan_id(示例 --input {\"test_plan_id\":\"1139...\"}),已跳过测试计划执行进度。'; return '测试计划执行进度(原始返回):\\n' + (typeof tp.data === 'string' ? tp.data : JSON.stringify(tp.data, null, 2)); })()"
|
|
68
|
+
depends_on: [stats]
|
|
69
|
+
|
|
70
|
+
report:
|
|
71
|
+
format: markdown
|
|
72
|
+
template: |
|
|
73
|
+
# 质量度量盘 — ${steps.header.data.name}
|
|
74
|
+
- 迭代区间: ${steps.header.data.range}
|
|
75
|
+
- 缺陷: 全量 ${steps.stats.data.total},未解 ${steps.stats.data.open}
|
|
76
|
+
- 需求(范围分母,已剔除 zzz 脏数据与【风险】项): ${steps.stats.data.stories}
|
|
77
|
+
- 缺陷密度(有效缺陷/需求,保留分母): ${steps.stats.data.densityText}
|
|
78
|
+
|
|
79
|
+
## severity × status 分布
|
|
80
|
+
${steps.distribution.data.matrixTable}
|
|
81
|
+
|
|
82
|
+
## 未解缺陷清单(${steps.open_bugs.data | length})
|
|
83
|
+
| id | 标题 | 严重度 | 状态 | 处理人 | 解决截止 |
|
|
84
|
+
| --- | --- | --- | --- | --- | --- |
|
|
85
|
+
${steps.open_bugs.data | rows id,title,severity,status,current_owner,deadline}
|
|
86
|
+
|
|
87
|
+
## 测试计划执行进度
|
|
88
|
+
${steps.testplan_note.data}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 缺陷分诊(S5 / 软考高项第12章 质量管理)
|
|
2
|
+
# 只读「建议模式」:本模板零写步骤,只产分配建议表;
|
|
3
|
+
# 确认后由人工执行 `tapd bug update <id> --owner <建议归属>`,或由 agent 调 tapd_update_bug 落库。
|
|
4
|
+
# 规则(对齐 templates/triage-bugs.yaml 的写法):
|
|
5
|
+
# 高严重度(vars.high_severity,默认 "1,2")→ 建议 te(测试口)复核;
|
|
6
|
+
# 其余未解缺陷 → 按 current_owner 现有未解负载做负载均衡(负载最小者优先)。
|
|
7
|
+
# 用法: tapd pm quality triage --iteration <id>
|
|
8
|
+
name: quality-triage
|
|
9
|
+
domain: quality
|
|
10
|
+
title: 缺陷分诊(建议模式,确认后人工/agent 执行 bug update)
|
|
11
|
+
write: false
|
|
12
|
+
unwrap: true
|
|
13
|
+
vars:
|
|
14
|
+
iteration_id: ""
|
|
15
|
+
high_severity: "1,2"
|
|
16
|
+
steps:
|
|
17
|
+
- id: bugs
|
|
18
|
+
uses: tapd_get_bugs
|
|
19
|
+
args:
|
|
20
|
+
iteration_id: "${vars.iteration_id}"
|
|
21
|
+
limit: 200
|
|
22
|
+
fields: "id,title,severity,status,priority_label,current_owner,te,de"
|
|
23
|
+
filter: "!item.title.startsWith('zzz-delete-me')"
|
|
24
|
+
|
|
25
|
+
- id: triage
|
|
26
|
+
expr: "(() => { const asArr = v => Array.isArray(v) ? v : []; const norm = v => String(v === undefined || v === null ? '' : v).trim(); const UNRESOLVED = ['closed', 'resolved', 'rejected', 'reject']; const open = asArr(steps.bugs.data).filter(b => !UNRESOLVED.includes(norm(b.status).toLowerCase())); const highs = norm(vars.high_severity).split(',').map(s => s.trim()).filter(Boolean); const isHigh = b => highs.includes(norm(b.severity)); const high = open.filter(isHigh); const rest = open.filter(b => !isHigh(b)); const owners = [...new Set(open.map(b => norm(b.current_owner)).filter(o => o !== ''))]; const load = {}; for (const o of owners) load[o] = 0; for (const b of rest) { const o = norm(b.current_owner); if (load[o] !== undefined) load[o] += 1; } const pickMin = () => { let best = null; for (const o of owners) { if (best === null || load[o] < load[best]) best = o; } return best; }; const plan = []; for (const b of high) { plan.push({ id: norm(b.id), title: norm(b.title), severity: norm(b.severity), priority_label: norm(b.priority_label), current_owner: norm(b.current_owner), suggested: norm(b.te) !== '' ? norm(b.te) : '(te 未指定,请人工确认)', rule: '高严重度→te' }); } for (const b of rest) { const target = owners.length > 0 ? pickMin() : '(无候选处理人,请人工指派)'; if (target !== null && load[target] !== undefined) load[target] += 1; plan.push({ id: norm(b.id), title: norm(b.title), severity: norm(b.severity), priority_label: norm(b.priority_label), current_owner: norm(b.current_owner), suggested: target, rule: '按 current_owner 负载均衡' }); } return { openCount: open.length, highCount: high.length, restCount: rest.length, owners, plan }; })()"
|
|
27
|
+
depends_on: [bugs]
|
|
28
|
+
|
|
29
|
+
report:
|
|
30
|
+
format: markdown
|
|
31
|
+
template: |
|
|
32
|
+
# 缺陷分诊(建议模式 — 确认后由人工/agent 执行 bug update,本模板零写步骤)
|
|
33
|
+
- 迭代: ${vars.iteration_id}
|
|
34
|
+
- 未解缺陷: ${steps.triage.data.openCount} 个 = 高严重度 ${steps.triage.data.highCount}(建议转 te 复核)+ 其余 ${steps.triage.data.restCount}(按 current_owner 负载均衡)
|
|
35
|
+
- 候选处理人池: ${steps.triage.data.owners | join}
|
|
36
|
+
|
|
37
|
+
## 分配建议表
|
|
38
|
+
| id | 标题 | 严重度 | 优先级 | 当前处理人 | 建议归属 | 规则 |
|
|
39
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
40
|
+
${steps.triage.data.plan | rows id,title,severity,priority_label,current_owner,suggested,rule}
|
|
41
|
+
|
|
42
|
+
> 建议模式说明:本场景只读不落库。确认后请人工执行 `tapd bug update <id> --owner <建议归属>`,或由 agent 调 tapd_update_bug 完成转派。
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# 风险登记(S5 / 软考高项第15章 风险管理)
|
|
2
|
+
# 写场景(含写步骤,默认 dry-run 闸门,--yes 确认落库):
|
|
3
|
+
# 以 tapd_create_story 落一条【风险】需求:name=【风险】<title>、label=risk、迭代关联、
|
|
4
|
+
# priority_label 按 probability 映射(高/High→High,中/Medium→Middle,低/Low→Low,未知→Middle)。
|
|
5
|
+
# 描述 markdown 由 expr 组装(类别 + 概率×影响 + 触发条件 + 应对策略 + 责任人),经 ${} 注入写步骤。
|
|
6
|
+
# 口径:登记后(label=risk 或名称以【风险】开头)会在范围度量中被剔除(pm scope/quality 模板内置过滤)。
|
|
7
|
+
# 用法: tapd pm risk register --iteration <id> --yes \
|
|
8
|
+
# --input '{"title":"联调环境不可用","category":"进度","probability":"高","impact":"里程碑延期","strategy":"规避","owner":"alice","trigger":"环境连续 3 天不可用"}'
|
|
9
|
+
name: risk-register
|
|
10
|
+
domain: risk
|
|
11
|
+
title: 风险登记(写:创建【风险】需求 label=risk,登记后会在范围度量中被剔除)
|
|
12
|
+
write: true
|
|
13
|
+
unwrap: true
|
|
14
|
+
vars:
|
|
15
|
+
iteration_id: ""
|
|
16
|
+
title: ""
|
|
17
|
+
category: ""
|
|
18
|
+
probability: ""
|
|
19
|
+
impact: ""
|
|
20
|
+
strategy: ""
|
|
21
|
+
owner: ""
|
|
22
|
+
trigger: ""
|
|
23
|
+
steps:
|
|
24
|
+
- id: assemble
|
|
25
|
+
expr: "(() => { const norm = v => String(v === undefined || v === null ? '' : v).trim(); const PMAP = { '高': 'High', 'high': 'High', 'h': 'High', '3': 'High', '中高': 'High', '中': 'Middle', 'medium': 'Middle', 'mid': 'Middle', 'm': 'Middle', '2': 'Middle', '低': 'Low', 'low': 'Low', 'l': 'Low', '1': 'Low' }; const p = norm(vars.probability).toLowerCase(); const priority_label = PMAP[p] || 'Middle'; const lines = ['【风险】' + norm(vars.title), '', '- 类别: ' + (norm(vars.category) || '未分类'), '- 概率×影响: ' + (norm(vars.probability) || '未填') + ' × ' + (norm(vars.impact) || '未填') + '(priority_label=' + priority_label + ')', '- 触发条件: ' + (norm(vars.trigger) || '未填'), '- 应对策略: ' + (norm(vars.strategy) || '未填'), '- 责任人: ' + (norm(vars.owner) || '未指定'), '- 关联迭代: ' + (norm(vars.iteration_id) || '未关联'), '- 登记说明: 由 tapd pm risk register 创建于 ' + today + ';label=risk,范围度量会剔除本项']; return { priority_label, probability: norm(vars.probability) || '未填', description: lines.join('\\n') }; })()"
|
|
26
|
+
|
|
27
|
+
# 缺 title / iteration_id 时跳过写步骤(宁可不落库,不产垃圾数据)
|
|
28
|
+
- id: register
|
|
29
|
+
uses: tapd_create_story
|
|
30
|
+
args:
|
|
31
|
+
name: "【风险】${vars.title}"
|
|
32
|
+
label: risk
|
|
33
|
+
iteration_id: "${vars.iteration_id}"
|
|
34
|
+
priority_label: "${steps.assemble.data.priority_label}"
|
|
35
|
+
description: "${steps.assemble.data.description}"
|
|
36
|
+
where: "String(vars.title || '').trim() !== '' && String(vars.iteration_id || '').trim() !== ''"
|
|
37
|
+
depends_on: [assemble]
|
|
38
|
+
|
|
39
|
+
- id: register_status
|
|
40
|
+
expr: "(() => { const titleOk = String(vars.title || '').trim() !== ''; const iterOk = String(vars.iteration_id || '').trim() !== ''; return { status: titleOk && iterOk ? '写步骤 register 已放行执行(闸门通过),成功时下方为新需求 id' : '写步骤 register 被跳过:缺少必填变量(title / iteration_id),请传 --iteration <id> 并在 --input 提供 title' }; })()"
|
|
41
|
+
depends_on: [assemble]
|
|
42
|
+
|
|
43
|
+
report:
|
|
44
|
+
format: markdown
|
|
45
|
+
template: |
|
|
46
|
+
# 风险登记结果(登记后会在范围度量中被剔除 — label=risk)
|
|
47
|
+
- 目标需求: 【风险】${vars.title}
|
|
48
|
+
- 迭代: ${vars.iteration_id};priority_label: ${steps.assemble.data.priority_label}(probability=${steps.assemble.data.probability})
|
|
49
|
+
- 状态: ${steps.register_status.data.status}
|
|
50
|
+
- 新需求 id: ${steps.register.data.id}
|
|
51
|
+
|
|
52
|
+
## 已写入的需求描述(markdown)
|
|
53
|
+
${steps.assemble.data.description}
|
|
54
|
+
|
|
55
|
+
> 口径说明:登记项带 label=risk(名称以【风险】开头),pm scope/quality 的范围度量会自动剔除;如需撤销请删除该需求或移除 risk 标签。
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# 风险扫描(S5 / 软考高项第15章 风险管理)
|
|
2
|
+
# 六路只读扫描,输出「候选风险清单」(类别/描述/证据/建议等级)供人工裁决:
|
|
3
|
+
# ① 逾期任务 tapd_get_tasks(due < today 且 status != done,zzz 过滤)
|
|
4
|
+
# ② 逾期缺陷 tapd_get_bugs(deadline < today 且未解,zzz 过滤)
|
|
5
|
+
# ③ 阻塞用例 tapd_get_tcase_result(result=block)(工作区级查询,无迭代维度)
|
|
6
|
+
# ④ 超支任务 tasks 里 exceed > 0
|
|
7
|
+
# ⑤ 依赖停滞 tapd_get_time_relative_stories 前置未完成(后置为迭代内需求)
|
|
8
|
+
# ⑥ 外部依赖 stories source 非空
|
|
9
|
+
# 本模板零写步骤:确认要跟踪的候选项后,用 `tapd pm risk register` 登记为【风险】需求。
|
|
10
|
+
# 用法: tapd pm risk scan --iteration <id>
|
|
11
|
+
name: risk-scan
|
|
12
|
+
domain: risk
|
|
13
|
+
title: 风险扫描(六路只读扫描,候选清单供人工裁决)
|
|
14
|
+
write: false
|
|
15
|
+
unwrap: true
|
|
16
|
+
vars:
|
|
17
|
+
iteration_id: ""
|
|
18
|
+
steps:
|
|
19
|
+
- id: tasks
|
|
20
|
+
uses: tapd_get_tasks
|
|
21
|
+
args:
|
|
22
|
+
iteration_id: "${vars.iteration_id}"
|
|
23
|
+
limit: 200
|
|
24
|
+
fields: "id,name,owner,status,due,effort,exceed"
|
|
25
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
26
|
+
|
|
27
|
+
- id: bugs
|
|
28
|
+
uses: tapd_get_bugs
|
|
29
|
+
args:
|
|
30
|
+
iteration_id: "${vars.iteration_id}"
|
|
31
|
+
limit: 200
|
|
32
|
+
fields: "id,title,severity,status,current_owner,deadline"
|
|
33
|
+
filter: "!item.title.startsWith('zzz-delete-me')"
|
|
34
|
+
|
|
35
|
+
- id: blocked
|
|
36
|
+
uses: tapd_get_tcase_result
|
|
37
|
+
# 真机约束(2026-09-11 L2 冒烟):tcase_result 必须带 tcase_id + test_plan_id(实体级查询),
|
|
38
|
+
# 工作区级"所有 block 用例"不可查。默认关闭;--input 提供 tcase_id/test_plan_id 时才启用本路。
|
|
39
|
+
where: "vars.tcase_id != null && vars.test_plan_id != null"
|
|
40
|
+
args:
|
|
41
|
+
tcase_id: "${vars.tcase_id}"
|
|
42
|
+
test_plan_id: "${vars.test_plan_id}"
|
|
43
|
+
result: block
|
|
44
|
+
limit: 200
|
|
45
|
+
fields: "id,tcase_id,test_plan_id,result,executed_by,executed"
|
|
46
|
+
|
|
47
|
+
- id: stories
|
|
48
|
+
uses: tapd_get_stories
|
|
49
|
+
args:
|
|
50
|
+
iteration_id: "${vars.iteration_id}"
|
|
51
|
+
limit: 200
|
|
52
|
+
fields: "id,name,status,owner,source"
|
|
53
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
54
|
+
|
|
55
|
+
- id: relations
|
|
56
|
+
uses: tapd_get_time_relative_stories
|
|
57
|
+
# 真机约束:无 story_id 的全量前后置查询 API 不接受。默认关闭;
|
|
58
|
+
# --input 提供 relations_story_id(迭代内某需求长 ID)时查该需求的依赖网络。
|
|
59
|
+
where: "vars.relations_story_id != null"
|
|
60
|
+
args:
|
|
61
|
+
story_id: "${vars.relations_story_id}"
|
|
62
|
+
limit: 200
|
|
63
|
+
fields: "predecessor_id,successor_id,relation_type"
|
|
64
|
+
|
|
65
|
+
- id: scan
|
|
66
|
+
expr: "(() => { const asArr = v => Array.isArray(v) ? v : []; const norm = v => String(v === undefined || v === null ? '' : v).trim(); const UNRESOLVED = ['closed', 'resolved', 'rejected', 'reject']; const DONEISH = ['done', 'closed', 'completed', '已关闭', '已完成']; const isDone = s => DONEISH.includes(norm(s).toLowerCase()); const tasks = asArr(steps.tasks.data); const bugs = asArr(steps.bugs.data); const blockedCases = asArr((steps['blocked'] || {}).data); const stories = asArr(steps.stories.data); const relations = asArr((steps['relations'] || {}).data); const findings = []; for (const t of tasks) { if (norm(t.due) !== '' && norm(t.due) < today && !isDone(t.status)) { findings.push({ category: '进度', kind: '逾期任务', id: norm(t.id), desc: '任务「' + norm(t.name) + '」逾期未完成(应完成 ' + norm(t.due) + ',状态 ' + (norm(t.status) || '-') + ')', evidence: 'task ' + norm(t.id) + ' due=' + norm(t.due) + ' status=' + norm(t.status) + ' owner=' + (norm(t.owner) || '-'), level: '高' }); } } for (const t of tasks) { const exceed = Number(t.exceed) || 0; if (exceed > 0) { const effort = Number(t.effort) || 0; const pct = effort > 0 ? Math.round((exceed / effort) * 100) : null; findings.push({ category: '成本', kind: '超支任务', id: norm(t.id), desc: '任务「' + norm(t.name) + '」超支 ' + exceed + ' 工时' + (pct !== null ? ('(超出预估 ' + pct + '%)') : ''), evidence: 'task ' + norm(t.id) + ' effort=' + (norm(t.effort) || '-') + ' exceed=' + exceed, level: pct !== null && pct >= 30 ? '高' : '中' }); } } for (const b of bugs) { if (norm(b.deadline) !== '' && norm(b.deadline) < today && !UNRESOLVED.includes(norm(b.status).toLowerCase())) { findings.push({ category: '质量', kind: '逾期缺陷', id: norm(b.id), desc: '缺陷「' + norm(b.title) + '」超过解决截止 ' + norm(b.deadline) + ' 仍未解(severity=' + (norm(b.severity) || '-') + ')', evidence: 'bug ' + norm(b.id) + ' deadline=' + norm(b.deadline) + ' status=' + norm(b.status) + ' owner=' + (norm(b.current_owner) || '-'), level: norm(b.severity) === '1' ? '高' : '中' }); } } for (const c of blockedCases) { findings.push({ category: '质量', kind: '阻塞用例', id: norm(c.tcase_id) || norm(c.id), desc: '测试用例执行被阻塞(result=block),需解除环境/依赖阻塞后回归', evidence: 'tcase ' + (norm(c.tcase_id) || norm(c.id)) + ' plan=' + (norm(c.test_plan_id) || '-') + ' by=' + (norm(c.executed_by) || '-') + ' at=' + (norm(c.executed) || '-'), level: '中' }); } const byId = new Map(stories.map(s => [norm(s.id), s])); const seenRel = new Set(); for (const r of relations) { const succ = byId.get(norm(r.successor_id)); if (!succ) continue; const pred = byId.get(norm(r.predecessor_id)); if (pred && isDone(pred.status)) continue; const key = norm(r.predecessor_id) + '->' + norm(r.successor_id); if (seenRel.has(key)) continue; seenRel.add(key); findings.push({ category: '依赖', kind: '依赖停滞', id: norm(succ.id), desc: '需求「' + norm(succ.name) + '」的前置「' + (pred ? norm(pred.name) : norm(r.predecessor_id) + '(迭代外)') + '」未完成(状态 ' + (pred ? (norm(pred.status) || '-') : '迭代外') + '),后置无法推进', evidence: 'relation pred=' + norm(r.predecessor_id) + ' succ=' + norm(r.successor_id) + ' type=' + (norm(r.relation_type) || '-'), level: '高' }); } for (const s of stories) { if (norm(s.source) !== '') { findings.push({ category: '依赖', kind: '外部依赖', id: norm(s.id), desc: '需求「' + norm(s.name) + '」来源外部(source=' + norm(s.source) + '),交付受外部制约,需跟踪', evidence: 'story ' + norm(s.id) + ' source=' + norm(s.source) + ' owner=' + (norm(s.owner) || '-'), level: '中' }); } } const catCount = {}; for (const f of findings) catCount[f.category] = (catCount[f.category] || 0) + 1; const categorySummary = Object.keys(catCount).map(k => k + ' ' + catCount[k]).join(','); return { today, total: findings.length, categorySummary, scanned: { tasks: tasks.length, bugs: bugs.length, blocked: blockedCases.length, stories: stories.length, relations: relations.length }, findings }; })()"
|
|
67
|
+
depends_on: [tasks, bugs, stories]
|
|
68
|
+
# blocked/relations 为 opt-in 路径(where 门控,缺省 skipped):刻意不用 depends_on 引用,
|
|
69
|
+
# 改为括号安全取数,避免「上游 skipped 级联跳过本聚合步」——与 quality-dashboard 同款处理。
|
|
70
|
+
|
|
71
|
+
report:
|
|
72
|
+
format: markdown
|
|
73
|
+
template: |
|
|
74
|
+
# 风险扫描报告(候选清单 — 输出供人工裁决,本模板零写步骤)
|
|
75
|
+
- 扫描日期: ${steps.scan.data.today};迭代: ${vars.iteration_id}
|
|
76
|
+
- 扫描量: 任务 ${steps.scan.data.scanned.tasks} / 缺陷 ${steps.scan.data.scanned.bugs} / 用例结果 ${steps.scan.data.scanned.blocked} / 需求 ${steps.scan.data.scanned.stories} / 前后置关系 ${steps.scan.data.scanned.relations}(zzz 脏数据已过滤)
|
|
77
|
+
- 候选风险: ${steps.scan.data.total} 项(${steps.scan.data.categorySummary})
|
|
78
|
+
|
|
79
|
+
## 候选清单(类别 / 描述 / 证据 / 建议等级)
|
|
80
|
+
| 类别 | 类型 | 对象 | 描述 | 证据 | 建议等级 |
|
|
81
|
+
| --- | --- | --- | --- | --- | --- |
|
|
82
|
+
${steps.scan.data.findings | rows category,kind,id,desc,evidence,level}
|
|
83
|
+
|
|
84
|
+
## 裁决与去向
|
|
85
|
+
- 确认要跟踪的候选项 → `tapd pm risk register --iteration <id> --yes --input '{"title":"...","category":"...","probability":"高|中|低","impact":"...","strategy":"...","owner":"...","trigger":"..."}'` 登记为【风险】需求(label=risk)
|
|
86
|
+
- 阻塞用例为工作区级查询(tapd_get_tcase_result 无迭代维度),请按 test_plan / tcase 上下文人工归位
|
|
87
|
+
- 建议等级仅为规则初判(高/中),最终等级以人工裁决为准;未确认的候选项不会落库
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# 迭代燃尽数据(软考第10章 进度管理,纯只读 L0)
|
|
2
|
+
# 剩余量按「完成日回放」近似:实体完成日取变更记录 status→done 的最早时间,
|
|
3
|
+
# 缺失时回退实体 completed 字段;按迭代 startdate~today 逐日推算剩余件数与剩余工时。
|
|
4
|
+
# 若变更接口不可用,趋势退化为快照日单点(report 脚注注明口径)。
|
|
5
|
+
# 用法: tapd pm schedule burndown [--iteration <id>] [--dry-run]
|
|
6
|
+
name: schedule-burndown
|
|
7
|
+
domain: schedule
|
|
8
|
+
title: 迭代燃尽数据(story/task 剩余量按日趋势 + 总量快照)
|
|
9
|
+
write: false
|
|
10
|
+
unwrap: true
|
|
11
|
+
vars:
|
|
12
|
+
iteration_id: ""
|
|
13
|
+
steps:
|
|
14
|
+
- id: iteration
|
|
15
|
+
uses: tapd_get_iterations
|
|
16
|
+
args:
|
|
17
|
+
id: "${vars.iteration_id}"
|
|
18
|
+
fields: "id,name,startdate,enddate,status"
|
|
19
|
+
|
|
20
|
+
- id: stories
|
|
21
|
+
uses: tapd_get_stories
|
|
22
|
+
args:
|
|
23
|
+
iteration_id: "${vars.iteration_id}"
|
|
24
|
+
fields: "id,name,owner,status,effort,effort_completed,remain,completed"
|
|
25
|
+
limit: 200
|
|
26
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
27
|
+
|
|
28
|
+
- id: tasks
|
|
29
|
+
uses: tapd_get_tasks
|
|
30
|
+
args:
|
|
31
|
+
iteration_id: "${vars.iteration_id}"
|
|
32
|
+
fields: "id,name,owner,status,effort,effort_completed,remain,completed"
|
|
33
|
+
limit: 200
|
|
34
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
35
|
+
|
|
36
|
+
- id: dates
|
|
37
|
+
expr: "(() => { return { today: today }; })()"
|
|
38
|
+
|
|
39
|
+
- id: story_ids
|
|
40
|
+
expr: "(steps.stories.data.length > 0 ? steps.stories.data.map(s => String(s.id)).join(',') : '0')"
|
|
41
|
+
depends_on: [stories]
|
|
42
|
+
|
|
43
|
+
- id: task_ids
|
|
44
|
+
expr: "(steps.tasks.data.length > 0 ? steps.tasks.data.map(t => String(t.id)).join(',') : '0')"
|
|
45
|
+
depends_on: [tasks]
|
|
46
|
+
|
|
47
|
+
- id: story_changes
|
|
48
|
+
uses: tapd_get_story_changes
|
|
49
|
+
args:
|
|
50
|
+
story_id: "${steps.story_ids.data}"
|
|
51
|
+
fields: "story_id,field,new_value,created"
|
|
52
|
+
limit: 200
|
|
53
|
+
depends_on: [story_ids]
|
|
54
|
+
|
|
55
|
+
- id: task_changes
|
|
56
|
+
uses: tapd_get_task_changes
|
|
57
|
+
args:
|
|
58
|
+
task_id: "${steps.task_ids.data}"
|
|
59
|
+
fields: "task_id,field,new_value,created"
|
|
60
|
+
limit: 200
|
|
61
|
+
depends_on: [task_ids]
|
|
62
|
+
|
|
63
|
+
- id: burndown
|
|
64
|
+
expr: "(() => { const arr = v => Array.isArray(v) ? v : (v && Array.isArray(v.data) ? v.data : []); const num = v => { const n = Number(v); return Number.isFinite(n) ? n : 0; }; const DONE = ['done', 'closed', 'close', 'completed', 'finish', 'finished', 'delivered', 'resolved']; const isDone = st => DONE.includes(String(st ?? '').toLowerCase()); const s = arr(steps.stories.data), t = arr(steps.tasks.data); const sc = arr(steps.story_changes.data), tc = arr(steps.task_changes.data); const td = steps.dates.data.today; const it = (steps.iteration.data ?? [])[0] ?? {}; const ents = [...s.map(x => ({ id: String(x.id), effort: num(x.effort), completed: x.completed ?? '' })), ...t.map(x => ({ id: String(x.id), effort: num(x.effort), completed: x.completed ?? '' }))]; const chg = [...sc.map(c => ({ id: String(c.story_id), field: c.field, nw: c.new_value, created: String(c.created ?? '').slice(0, 10) })), ...tc.map(c => ({ id: String(c.task_id), field: c.field, nw: c.new_value, created: String(c.created ?? '').slice(0, 10) }))]; const dayMap = {}; for (const e of ents) { const hits = chg.filter(c => c.id === e.id && c.field === 'status' && isDone(c.nw) && c.created !== '').map(c => c.created).sort(); const fb = String(e.completed ?? '').slice(0, 10); dayMap[e.id] = hits.length > 0 ? hits[0] : (fb !== '' ? fb : ''); } const totalEffort = ents.reduce((a, e) => a + e.effort, 0); let cur = it.startdate ? String(it.startdate).slice(0, 10) : td; const days = []; const nextDay = d => new Date(new Date(d + 'T00:00:00Z').getTime() + 86400000).toISOString().slice(0, 10); let guard = 0; while (cur <= td && guard < 120) { days.push(cur); cur = nextDay(cur); guard++; } if (days.length === 0) days.push(td); const doneByDay = e => { const dd = dayMap[e.id]; return dd !== undefined && dd !== '' && dd <= days[days.length - 1] ? dd : ''; }; const trend = days.map(d => { const doneList = ents.filter(e => { const dd = doneByDay(e); return dd !== '' && dd <= d; }); const burn = doneList.reduce((a, e) => a + e.effort, 0); return { date: d, doneEntities: doneList.length, remainEntities: ents.length - doneList.length, burnEffort: Math.round(burn * 100) / 100, remainEffort: Math.round((totalEffort - burn) * 100) / 100 }; }); const dist = days.map((d, i) => ({ date: d, completed: trend[i].doneEntities - (i > 0 ? trend[i - 1].doneEntities : 0) })); const all = [...s, ...t]; return { totals: { totalStories: s.length, totalTasks: t.length, doneStories: s.filter(x => isDone(x.status)).length, doneTasks: t.filter(x => isDone(x.status)).length, totalEffort: totalEffort, effortCompleted: all.reduce((a, x) => a + num(x.effort_completed), 0), remain: all.reduce((a, x) => a + num(x.remain), 0) }, trend: trend, dist: dist, meta: { from: days[0], to: days[days.length - 1], days: days.length, entities: ents.length } }; })()"
|
|
65
|
+
depends_on: [iteration, dates, story_changes, task_changes]
|
|
66
|
+
|
|
67
|
+
report:
|
|
68
|
+
format: markdown
|
|
69
|
+
template: |
|
|
70
|
+
# 迭代燃尽数据 — ${steps.iteration.data.0.name}(${steps.iteration.data.0.startdate} ~ ${steps.iteration.data.0.enddate})
|
|
71
|
+
|
|
72
|
+
- 总量快照: 需求 ${steps.burndown.data.totals.totalStories}(done ${steps.burndown.data.totals.doneStories}),任务 ${steps.burndown.data.totals.totalTasks}(done ${steps.burndown.data.totals.doneTasks})
|
|
73
|
+
- 工时: 总预估 ${steps.burndown.data.totals.totalEffort} h / 已完成 ${steps.burndown.data.totals.effortCompleted} h / 剩余 ${steps.burndown.data.totals.remain} h
|
|
74
|
+
- 趋势窗口: ${steps.burndown.data.meta.from} ~ ${steps.burndown.data.meta.to}(${steps.burndown.data.meta.days} 天,${steps.burndown.data.meta.entities} 个实体,已剔除 zzz-delete-me* 测试数据)
|
|
75
|
+
|
|
76
|
+
## 按日趋势(完成日回放)
|
|
77
|
+
| 日期 | 累计完成(件) | 剩余(件) | 累计消耗工时(h) | 剩余工时(h) |
|
|
78
|
+
|---|---|---|---|---|
|
|
79
|
+
${steps.burndown.data.trend | rows date,doneEntities,remainEntities,burnEffort,remainEffort}
|
|
80
|
+
|
|
81
|
+
## 完成量按日分布
|
|
82
|
+
| 日期 | 当日完成(件) |
|
|
83
|
+
|---|---|
|
|
84
|
+
${steps.burndown.data.dist | rows date,completed}
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
口径:剩余量按「完成日回放」近似,非实时小时级燃尽——实体完成日取变更记录 status→done 的最早时间,缺失时回退实体 completed 字段;当日消耗工时按实体 effort 预估整额计入完成日(故与 effort_completed 口径的快照值可能不等);趋势窗口取迭代 startdate~今日(上限 120 天,未来开始的迭代退化为今日单点);变更查询按迭代实体 id 合并多 ID 查询(limit 200,超限截断不翻页)。
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# 站会三段式材料(软考第10章 进度管理,纯只读 L0)
|
|
2
|
+
# 三段式:昨日完成 / 今日计划 / 阻塞扫描,全部按人(owner)分组
|
|
3
|
+
# 数据源:迭代 stories/tasks/bugs 快照 + story_changes/task_changes(按迭代实体 id 列表
|
|
4
|
+
# 合并成 TAPD 多 ID 查询,逗号分隔,控制步骤规模为 2 次 changes 调用而非逐实体查询)
|
|
5
|
+
# 用法: tapd pm schedule standup [--iteration <id>] [--dry-run]
|
|
6
|
+
name: schedule-standup
|
|
7
|
+
domain: schedule
|
|
8
|
+
title: 站会三段式材料(昨日变更/今日计划/阻塞扫描,按人分组)
|
|
9
|
+
write: false
|
|
10
|
+
unwrap: true
|
|
11
|
+
vars:
|
|
12
|
+
iteration_id: ""
|
|
13
|
+
steps:
|
|
14
|
+
- id: iteration
|
|
15
|
+
uses: tapd_get_iterations
|
|
16
|
+
args:
|
|
17
|
+
id: "${vars.iteration_id}"
|
|
18
|
+
fields: "id,name,startdate,enddate,status"
|
|
19
|
+
|
|
20
|
+
- id: stories
|
|
21
|
+
uses: tapd_get_stories
|
|
22
|
+
args:
|
|
23
|
+
iteration_id: "${vars.iteration_id}"
|
|
24
|
+
fields: "id,name,owner,status,begin,due"
|
|
25
|
+
limit: 200
|
|
26
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
27
|
+
|
|
28
|
+
- id: tasks
|
|
29
|
+
uses: tapd_get_tasks
|
|
30
|
+
args:
|
|
31
|
+
iteration_id: "${vars.iteration_id}"
|
|
32
|
+
fields: "id,name,owner,status,progress,begin,due"
|
|
33
|
+
limit: 200
|
|
34
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
35
|
+
|
|
36
|
+
- id: bugs
|
|
37
|
+
uses: tapd_get_bugs
|
|
38
|
+
args:
|
|
39
|
+
iteration_id: "${vars.iteration_id}"
|
|
40
|
+
fields: "id,title,current_owner,status"
|
|
41
|
+
limit: 200
|
|
42
|
+
filter: "!item.title.startsWith('zzz-delete-me')"
|
|
43
|
+
|
|
44
|
+
- id: dates
|
|
45
|
+
expr: "(() => { const fmt = ms => new Date(ms).toISOString().slice(0, 10); const base = new Date(today + 'T00:00:00Z').getTime(); return { today: today, yesterday: fmt(base - 86400000) }; })()"
|
|
46
|
+
|
|
47
|
+
- id: story_ids
|
|
48
|
+
expr: "(steps.stories.data.length > 0 ? steps.stories.data.map(s => String(s.id)).join(',') : '0')"
|
|
49
|
+
depends_on: [stories]
|
|
50
|
+
|
|
51
|
+
- id: task_ids
|
|
52
|
+
expr: "(steps.tasks.data.length > 0 ? steps.tasks.data.map(t => String(t.id)).join(',') : '0')"
|
|
53
|
+
depends_on: [tasks]
|
|
54
|
+
|
|
55
|
+
- id: story_changes
|
|
56
|
+
uses: tapd_get_story_changes
|
|
57
|
+
args:
|
|
58
|
+
story_id: "${steps.story_ids.data}"
|
|
59
|
+
fields: "story_id,field,old_value,new_value,created,author"
|
|
60
|
+
limit: 200
|
|
61
|
+
depends_on: [story_ids]
|
|
62
|
+
|
|
63
|
+
- id: task_changes
|
|
64
|
+
uses: tapd_get_task_changes
|
|
65
|
+
args:
|
|
66
|
+
task_id: "${steps.task_ids.data}"
|
|
67
|
+
fields: "task_id,field,old_value,new_value,created,author"
|
|
68
|
+
limit: 200
|
|
69
|
+
depends_on: [task_ids]
|
|
70
|
+
|
|
71
|
+
- id: standup
|
|
72
|
+
expr: "(() => { const arr = v => Array.isArray(v) ? v : (v && Array.isArray(v.data) ? v.data : []); const num = v => { const n = Number(String(v ?? '').replace('%', '')); return Number.isFinite(n) ? n : 0; }; const ownerOf = v => { const s = String(v ?? '').trim(); return s === '' ? '(未分派)' : s; }; const DONE = ['done', 'closed', 'close', 'completed', 'finish', 'finished', 'delivered', 'resolved']; const isDone = st => DONE.includes(String(st ?? '').toLowerCase()); const s = arr(steps.stories.data), t = arr(steps.tasks.data), b = arr(steps.bugs.data); const sc = arr(steps.story_changes.data), tc = arr(steps.task_changes.data); const y = steps.dates.data.yesterday, td = steps.dates.data.today; const evts = [...sc.map(c => ({ kind: 'story', id: String(c.story_id), field: c.field, nw: c.new_value, created: c.created, author: c.author })), ...tc.map(c => ({ kind: 'task', id: String(c.task_id), field: c.field, nw: c.new_value, created: c.created, author: c.author }))]; const recent = evts.filter(c => String(c.created ?? '').slice(0, 10) >= y); const doneEvts = recent.filter(c => c.field === 'status' && isDone(c.nw)); const doneRows = []; const seen = new Set(); for (const e of doneEvts) { const key = e.kind + ':' + e.id; if (seen.has(key)) continue; seen.add(key); const ent = e.kind === 'story' ? s.find(x => String(x.id) === e.id) : t.find(x => String(x.id) === e.id); doneRows.push({ owner: ownerOf(ent ? ent.owner : ''), kind: e.kind, id: e.id, name: ent ? ent.name : '(迭代外实体)', author: e.author ?? '', at: String(e.created ?? '').slice(0, 10) }); } const planRows = t.filter(x => !isDone(x.status)).map(x => ({ owner: ownerOf(x.owner), id: String(x.id), name: x.name, status: x.status, progress: num(x.progress), begin: x.begin ?? '', due: x.due ?? '' })); const blockRows = planRows.filter(r => r.progress === 0 && r.begin !== '' && r.begin < td).map(r => ({ owner: r.owner, id: r.id, name: r.name, begin: r.begin, due: r.due, status: r.status })); const bugMap = {}; for (const g of b) { if (isDone(g.status)) continue; const o = ownerOf(g.current_owner); bugMap[o] = (bugMap[o] ?? 0) + 1; } const bugRows = Object.keys(bugMap).sort().map(o => ({ owner: o, openBugs: bugMap[o] })); const owners = [...new Set([...doneRows.map(r => r.owner), ...planRows.map(r => r.owner), ...blockRows.map(r => r.owner), ...bugRows.map(r => r.owner)])].sort(); const summary = owners.map(o => ({ owner: o, doneYesterday: doneRows.filter(r => r.owner === o).length, plannedToday: planRows.filter(r => r.owner === o).length, blocked: blockRows.filter(r => r.owner === o).length, openBugs: bugMap[o] ?? 0 })); return { summary: summary, doneRows: doneRows, planRows: planRows, blockRows: blockRows, bugRows: bugRows, counts: { today: td, yesterday: y, changes: recent.length, done: doneRows.length, planned: planRows.length, blocked: blockRows.length, storyCount: s.length, taskCount: t.length, bugCount: b.length } }; })()"
|
|
73
|
+
depends_on: [iteration, bugs, dates, story_changes, task_changes]
|
|
74
|
+
|
|
75
|
+
report:
|
|
76
|
+
format: markdown
|
|
77
|
+
template: |
|
|
78
|
+
# 站会三段式材料 — ${steps.iteration.data.0.name}(${steps.iteration.data.0.startdate} ~ ${steps.iteration.data.0.enddate})
|
|
79
|
+
|
|
80
|
+
- 数据窗口: 昨日 ${steps.standup.data.counts.yesterday} → 今日 ${steps.standup.data.counts.today}
|
|
81
|
+
- 范围: 需求 ${steps.standup.data.counts.storyCount} / 任务 ${steps.standup.data.counts.taskCount} / 缺陷 ${steps.standup.data.counts.bugCount}(已剔除 zzz-delete-me* 测试数据);昨日以来变更事件 ${steps.standup.data.counts.changes} 条
|
|
82
|
+
|
|
83
|
+
## 按人汇总
|
|
84
|
+
| 处理人 | 昨日完成 | 今日计划 | 阻塞 | 未解 bug |
|
|
85
|
+
|---|---|---|---|---|
|
|
86
|
+
${steps.standup.data.summary | rows owner,doneYesterday,plannedToday,blocked,openBugs}
|
|
87
|
+
|
|
88
|
+
## 一、昨日完成(${steps.standup.data.counts.done})
|
|
89
|
+
| 处理人 | 类型 | id | 名称 | 变更人 | 完成日 |
|
|
90
|
+
|---|---|---|---|---|---|
|
|
91
|
+
${steps.standup.data.doneRows | rows owner,kind,id,name,author,at}
|
|
92
|
+
|
|
93
|
+
## 二、今日计划(${steps.standup.data.counts.planned})
|
|
94
|
+
| 处理人 | id | 任务 | 状态 | 进度% | begin | due |
|
|
95
|
+
|---|---|---|---|---|---|---|
|
|
96
|
+
${steps.standup.data.planRows | rows owner,id,name,status,progress,begin,due}
|
|
97
|
+
|
|
98
|
+
## 三、阻塞扫描(${steps.standup.data.counts.blocked})
|
|
99
|
+
| 处理人 | id | 任务 | begin | due | 状态 |
|
|
100
|
+
|---|---|---|---|---|---|
|
|
101
|
+
${steps.standup.data.blockRows | rows owner,id,name,begin,due,status}
|
|
102
|
+
|
|
103
|
+
## 未解 bug 分布
|
|
104
|
+
| 处理人 | 未解 bug |
|
|
105
|
+
|---|---|
|
|
106
|
+
${steps.standup.data.bugRows | rows owner,openBugs}
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
口径:昨日完成 = story/task 变更记录中 status → done 类状态且 created >= 昨日(每实体去重取首条,按实体当前 owner 归组);今日计划 = 迭代内未完成任务;阻塞 = 任务 progress=0 且 begin < 今日;未解 bug = 状态不在 done/closed/... 的迭代内缺陷。变更查询按迭代实体 id 列表合并多 ID 查询(limit 200,超限截断不翻页;空迭代以 id=0 占位保证步骤可执行)。
|