@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.
Files changed (175) hide show
  1. package/README.md +22 -0
  2. package/dist/advisor/domain-axis.d.ts +16 -0
  3. package/dist/advisor/domain-axis.d.ts.map +1 -0
  4. package/dist/advisor/domain-axis.js +43 -0
  5. package/dist/advisor/domain-axis.js.map +1 -0
  6. package/dist/advisor/domain-axis.test.d.ts +2 -0
  7. package/dist/advisor/domain-axis.test.d.ts.map +1 -0
  8. package/dist/advisor/domain-axis.test.js +198 -0
  9. package/dist/advisor/domain-axis.test.js.map +1 -0
  10. package/dist/advisor/index.d.ts +8 -0
  11. package/dist/advisor/index.d.ts.map +1 -0
  12. package/dist/advisor/index.js +6 -0
  13. package/dist/advisor/index.js.map +1 -0
  14. package/dist/advisor/persona.d.ts +12 -0
  15. package/dist/advisor/persona.d.ts.map +1 -0
  16. package/dist/advisor/persona.js +34 -0
  17. package/dist/advisor/persona.js.map +1 -0
  18. package/dist/advisor/recall.d.ts +38 -0
  19. package/dist/advisor/recall.d.ts.map +1 -0
  20. package/dist/advisor/recall.js +96 -0
  21. package/dist/advisor/recall.js.map +1 -0
  22. package/dist/advisor/render.d.ts +8 -0
  23. package/dist/advisor/render.d.ts.map +1 -0
  24. package/dist/advisor/render.js +42 -0
  25. package/dist/advisor/render.js.map +1 -0
  26. package/dist/commands/advisor.d.ts.map +1 -1
  27. package/dist/commands/advisor.js +31 -13
  28. package/dist/commands/advisor.js.map +1 -1
  29. package/dist/commands/pipeline.d.ts +4 -0
  30. package/dist/commands/pipeline.d.ts.map +1 -0
  31. package/dist/commands/pipeline.js +178 -0
  32. package/dist/commands/pipeline.js.map +1 -0
  33. package/dist/commands/pm.d.ts +4 -0
  34. package/dist/commands/pm.d.ts.map +1 -0
  35. package/dist/commands/pm.js +175 -0
  36. package/dist/commands/pm.js.map +1 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +8 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/pipeline/engine.d.ts +38 -0
  42. package/dist/pipeline/engine.d.ts.map +1 -0
  43. package/dist/pipeline/engine.js +326 -0
  44. package/dist/pipeline/engine.js.map +1 -0
  45. package/dist/pipeline/engine.test.d.ts +2 -0
  46. package/dist/pipeline/engine.test.d.ts.map +1 -0
  47. package/dist/pipeline/engine.test.js +419 -0
  48. package/dist/pipeline/engine.test.js.map +1 -0
  49. package/dist/pipeline/expression.d.ts +31 -0
  50. package/dist/pipeline/expression.d.ts.map +1 -0
  51. package/dist/pipeline/expression.js +249 -0
  52. package/dist/pipeline/expression.js.map +1 -0
  53. package/dist/pipeline/expression.test.d.ts +2 -0
  54. package/dist/pipeline/expression.test.d.ts.map +1 -0
  55. package/dist/pipeline/expression.test.js +111 -0
  56. package/dist/pipeline/expression.test.js.map +1 -0
  57. package/dist/pipeline/history.d.ts +13 -0
  58. package/dist/pipeline/history.d.ts.map +1 -0
  59. package/dist/pipeline/history.js +66 -0
  60. package/dist/pipeline/history.js.map +1 -0
  61. package/dist/pipeline/index.d.ts +13 -0
  62. package/dist/pipeline/index.d.ts.map +1 -0
  63. package/dist/pipeline/index.js +9 -0
  64. package/dist/pipeline/index.js.map +1 -0
  65. package/dist/pipeline/record.d.ts +13 -0
  66. package/dist/pipeline/record.d.ts.map +1 -0
  67. package/dist/pipeline/record.js +30 -0
  68. package/dist/pipeline/record.js.map +1 -0
  69. package/dist/pipeline/record.test.d.ts +2 -0
  70. package/dist/pipeline/record.test.d.ts.map +1 -0
  71. package/dist/pipeline/record.test.js +109 -0
  72. package/dist/pipeline/record.test.js.map +1 -0
  73. package/dist/pipeline/template.d.ts +24 -0
  74. package/dist/pipeline/template.d.ts.map +1 -0
  75. package/dist/pipeline/template.js +134 -0
  76. package/dist/pipeline/template.js.map +1 -0
  77. package/dist/pipeline/template.test.d.ts +2 -0
  78. package/dist/pipeline/template.test.d.ts.map +1 -0
  79. package/dist/pipeline/template.test.js +94 -0
  80. package/dist/pipeline/template.test.js.map +1 -0
  81. package/dist/pipeline/types.d.ts +99 -0
  82. package/dist/pipeline/types.d.ts.map +1 -0
  83. package/dist/pipeline/types.js +31 -0
  84. package/dist/pipeline/types.js.map +1 -0
  85. package/dist/pipeline/unwrap.d.ts +3 -0
  86. package/dist/pipeline/unwrap.d.ts.map +1 -0
  87. package/dist/pipeline/unwrap.js +26 -0
  88. package/dist/pipeline/unwrap.js.map +1 -0
  89. package/dist/pipeline/unwrap.test.d.ts +2 -0
  90. package/dist/pipeline/unwrap.test.d.ts.map +1 -0
  91. package/dist/pipeline/unwrap.test.js +36 -0
  92. package/dist/pipeline/unwrap.test.js.map +1 -0
  93. package/dist/pipeline/validate.d.ts +18 -0
  94. package/dist/pipeline/validate.d.ts.map +1 -0
  95. package/dist/pipeline/validate.js +134 -0
  96. package/dist/pipeline/validate.js.map +1 -0
  97. package/dist/pipeline/validate.test.d.ts +2 -0
  98. package/dist/pipeline/validate.test.d.ts.map +1 -0
  99. package/dist/pipeline/validate.test.js +166 -0
  100. package/dist/pipeline/validate.test.js.map +1 -0
  101. package/dist/pm/discovery.d.ts +54 -0
  102. package/dist/pm/discovery.d.ts.map +1 -0
  103. package/dist/pm/discovery.js +198 -0
  104. package/dist/pm/discovery.js.map +1 -0
  105. package/dist/pm/discovery.test.d.ts +2 -0
  106. package/dist/pm/discovery.test.d.ts.map +1 -0
  107. package/dist/pm/discovery.test.js +147 -0
  108. package/dist/pm/discovery.test.js.map +1 -0
  109. package/dist/pm/domains.d.ts +15 -0
  110. package/dist/pm/domains.d.ts.map +1 -0
  111. package/dist/pm/domains.js +27 -0
  112. package/dist/pm/domains.js.map +1 -0
  113. package/dist/pm/domains.test.d.ts +2 -0
  114. package/dist/pm/domains.test.d.ts.map +1 -0
  115. package/dist/pm/domains.test.js +141 -0
  116. package/dist/pm/domains.test.js.map +1 -0
  117. package/dist/pm/index.d.ts +9 -0
  118. package/dist/pm/index.d.ts.map +1 -0
  119. package/dist/pm/index.js +6 -0
  120. package/dist/pm/index.js.map +1 -0
  121. package/dist/pm/iteration.d.ts +26 -0
  122. package/dist/pm/iteration.d.ts.map +1 -0
  123. package/dist/pm/iteration.js +50 -0
  124. package/dist/pm/iteration.js.map +1 -0
  125. package/dist/pm/iteration.test.d.ts +2 -0
  126. package/dist/pm/iteration.test.d.ts.map +1 -0
  127. package/dist/pm/iteration.test.js +125 -0
  128. package/dist/pm/iteration.test.js.map +1 -0
  129. package/dist/pm/pm-command.test.d.ts +2 -0
  130. package/dist/pm/pm-command.test.d.ts.map +1 -0
  131. package/dist/pm/pm-command.test.js +321 -0
  132. package/dist/pm/pm-command.test.js.map +1 -0
  133. package/dist/pm/render.d.ts +17 -0
  134. package/dist/pm/render.d.ts.map +1 -0
  135. package/dist/pm/render.js +82 -0
  136. package/dist/pm/render.js.map +1 -0
  137. package/dist/pm/tpl-communication-stakeholder.test.d.ts +2 -0
  138. package/dist/pm/tpl-communication-stakeholder.test.d.ts.map +1 -0
  139. package/dist/pm/tpl-communication-stakeholder.test.js +392 -0
  140. package/dist/pm/tpl-communication-stakeholder.test.js.map +1 -0
  141. package/dist/pm/tpl-integration-scope.test.d.ts +2 -0
  142. package/dist/pm/tpl-integration-scope.test.d.ts.map +1 -0
  143. package/dist/pm/tpl-integration-scope.test.js +387 -0
  144. package/dist/pm/tpl-integration-scope.test.js.map +1 -0
  145. package/dist/pm/tpl-quality-risk.test.d.ts +2 -0
  146. package/dist/pm/tpl-quality-risk.test.d.ts.map +1 -0
  147. package/dist/pm/tpl-quality-risk.test.js +414 -0
  148. package/dist/pm/tpl-quality-risk.test.js.map +1 -0
  149. package/dist/pm/tpl-schedule-cost.test.d.ts +2 -0
  150. package/dist/pm/tpl-schedule-cost.test.d.ts.map +1 -0
  151. package/dist/pm/tpl-schedule-cost.test.js +376 -0
  152. package/dist/pm/tpl-schedule-cost.test.js.map +1 -0
  153. package/dist/program.d.ts.map +1 -1
  154. package/dist/program.js +6 -0
  155. package/dist/program.js.map +1 -1
  156. package/package.json +11 -6
  157. package/templates/deliver-story.yaml +38 -0
  158. package/templates/iteration-review.yaml +52 -0
  159. package/templates/pm/README.md +57 -0
  160. package/templates/pm/communication-notify.yaml +65 -0
  161. package/templates/pm/communication-weekly.yaml +160 -0
  162. package/templates/pm/cost-report.yaml +74 -0
  163. package/templates/pm/integration-close.yaml +118 -0
  164. package/templates/pm/integration-health.yaml +155 -0
  165. package/templates/pm/integration-kickoff.yaml +94 -0
  166. package/templates/pm/quality-dashboard.yaml +88 -0
  167. package/templates/pm/quality-triage.yaml +42 -0
  168. package/templates/pm/risk-register.yaml +55 -0
  169. package/templates/pm/risk-scan.yaml +87 -0
  170. package/templates/pm/schedule-burndown.yaml +87 -0
  171. package/templates/pm/schedule-standup.yaml +109 -0
  172. package/templates/pm/scope-baseline.yaml +93 -0
  173. package/templates/pm/scope-drift-check.yaml +129 -0
  174. package/templates/pm/stakeholder-map.yaml +121 -0
  175. 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 占位保证步骤可执行)。