@xihe-lab/tapd-cli 2.0.0-rc.1 → 2.0.0-rc.3
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/bin/tapd.js +0 -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 +17 -11
- 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,57 @@
|
|
|
1
|
+
# templates/pm/ — pm 管理语义层场景模板目录
|
|
2
|
+
|
|
3
|
+
本目录随包发布,存放 `tapd pm <domain> <scenario>` 场景的 pipeline 模板。
|
|
4
|
+
S2 只交付框架(命令组 / 发现 / 别名 / 写闸门),**域场景模板由 S3-S6 各波次落盘到本目录**,
|
|
5
|
+
框架侧零注册代码:yaml 文件放进来即被 `tapd pm --help` 分组、`tapd pm list` 与别名执行收录。
|
|
6
|
+
|
|
7
|
+
## 文件命名约定(硬约束)
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
<domain>-<scenario>.yaml 例:risk-scan.yaml、scope-drift-check.yaml
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `<domain>` 必须是域表八键之一(小写):
|
|
14
|
+
`integration | scope | schedule | cost | quality | communication | risk | stakeholder`
|
|
15
|
+
(对应软考高项第4版 第8/9/10/11/12/14/15/17 章;采购第16章、绩效第18章不设域,见设计文档 §2)
|
|
16
|
+
- `<scenario>` 为场景 slug,允许连字符(如 `drift-check`);别名解析按**首个 `-`** 拆分,因此域键不能含 `-`
|
|
17
|
+
- 扩展名支持 `.yaml` / `.yml` / `.json`
|
|
18
|
+
|
|
19
|
+
## frontmatter 字段
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
name: risk-scan # 模板名,约定与文件名 stem 一致
|
|
23
|
+
domain: risk # pm 层扩展字段:域键(执行前由 pm 层剥离,S1 严格 schema 不认识它)
|
|
24
|
+
title: 风险扫描(软考第15章) # help/list 展示
|
|
25
|
+
write: false # 写闸门依据:含写步骤必须 true(S1 validate 强制)
|
|
26
|
+
vars: # 变量缺省值,可被 pm flags 覆盖
|
|
27
|
+
iteration_id: ""
|
|
28
|
+
steps: [] # 与 S1 pipeline 模板完全同构(uses/expr/args/where/filter/depends_on)
|
|
29
|
+
report: {} # format: markdown|text|json + template 插值
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
除 `domain` 外的全部字段与 `tapd pipeline` 模板 schema 一致(见 `src/pipeline/template.ts`)。
|
|
33
|
+
|
|
34
|
+
## 可用 vars 与注入
|
|
35
|
+
|
|
36
|
+
| 变量 | 来源 | 说明 |
|
|
37
|
+
|------|------|------|
|
|
38
|
+
| `${vars.iteration_id}` | `--iteration <id>`;缺省时 pm 自动探测:`tapd_get_iterations status=open`,过滤 `zzz-delete-me*`,按 startdate 取最新 | 迭代作用域,缺省注入到 vars |
|
|
39
|
+
| workspace_id | 经 `defaultArgs` 兜底注入每个工具步骤(全局 `--workspace-id`),步骤 args 显式声明优先 | 无需在模板里写 |
|
|
40
|
+
|
|
41
|
+
## 数据口径(模板内置,代码化的 skill 前置检查)
|
|
42
|
+
|
|
43
|
+
- 迭代/需求/缺陷类步骤加 `filter: "!item.name.startsWith('zzz-delete-me')"`(缺陷用 title)
|
|
44
|
+
- 比率类输出保留分母,不因过滤误导
|
|
45
|
+
|
|
46
|
+
## 用户覆盖
|
|
47
|
+
|
|
48
|
+
同名文件放 `~/.tapd/templates/pm/`(或 `TAPD_USER_TEMPLATES_DIR` 指定的根目录下的 `pm/`)即覆盖内置同名场景;
|
|
49
|
+
`tapd pm list` 的 `source` 列标记来源。
|
|
50
|
+
|
|
51
|
+
## 落盘自检
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
tapd pm list # 新场景应出现在清单
|
|
55
|
+
tapd pm <domain> <scenario> --dry-run # 计划预览 + 写闸门
|
|
56
|
+
tapd pipeline validate <file> # S1 静态校验(注意:含 domain 字段时请用 tapd pm 执行)
|
|
57
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 沟通管理 · 变更通知(含写步骤:tapd_create_comment,默认写闸门拦截,--yes 确认落库)
|
|
2
|
+
# 用法: tapd pm communication notify --input '{"entry_type":"stories","entry_id":"1140...","message":"...","cc":"alice|bob"}' [--yes]
|
|
3
|
+
# 等价: tapd pipeline run templates/pm/communication-notify.yaml --input '...' --yes
|
|
4
|
+
# 措辞纪律: message 为纯文本时自动包一层 <p>(已含 HTML 标签则原样透传);
|
|
5
|
+
# **措辞必须人审后 --yes 落库**——无 --yes 时仅输出将创建的评论预览,不触 API。
|
|
6
|
+
# 富媒体(rc.2): message 文本里可直接写 @昵称——服务端智能识别并触发通知(R4);
|
|
7
|
+
# 如需 at-who 角标/附件锚点/图片,先用 tapd_md_to_html 预转换(@昵称→at-who、
|
|
8
|
+
# [📎 名称](attach:<ws>/<id>)→附件锚点、--upload-images 顺带上传本地图片),
|
|
9
|
+
# 再把得到的 HTML 整段作为 message 传入(已含 HTML 标签则本模板原样透传,不二次包裹)。
|
|
10
|
+
# 关键通知场景建议同时填 cc(@ 校验/通知不依赖本地成员名单)。
|
|
11
|
+
name: communication-notify
|
|
12
|
+
domain: communication
|
|
13
|
+
title: 变更通知(需求/缺陷/任务上留言并抄送;措辞必须人审后 --yes)
|
|
14
|
+
write: true
|
|
15
|
+
unwrap: true
|
|
16
|
+
vars:
|
|
17
|
+
entry_type: "stories"
|
|
18
|
+
entry_id: ""
|
|
19
|
+
message: ""
|
|
20
|
+
cc: ""
|
|
21
|
+
steps:
|
|
22
|
+
# ---- 本地整理:entry_type 归一化 + 纯文本包 <p>(不触 API、永不判写)----
|
|
23
|
+
- id: prep
|
|
24
|
+
expr: |-
|
|
25
|
+
(() => {
|
|
26
|
+
const raw = String(vars.message || '');
|
|
27
|
+
const isHtml = /^\s*<[a-z][a-z0-9]*\b/i.test(raw);
|
|
28
|
+
const alias = { story: 'stories', stories: 'stories', task: 'tasks', tasks: 'tasks', bug: 'bug', bug_remark: 'bug_remark' };
|
|
29
|
+
const type = alias[String(vars.entry_type || '').trim().toLowerCase()] || String(vars.entry_type || '').trim();
|
|
30
|
+
const cc = String(vars.cc || '').trim();
|
|
31
|
+
return {
|
|
32
|
+
ready: raw.trim() !== '' && String(vars.entry_id || '').trim() !== '',
|
|
33
|
+
entry_type: type,
|
|
34
|
+
entry_id: String(vars.entry_id || '').trim(),
|
|
35
|
+
description: isHtml ? raw : '<p>' + raw.trim() + '</p>',
|
|
36
|
+
cc: cc,
|
|
37
|
+
wrapped_label: isHtml ? '原文已是 HTML,未二次包裹' : '纯文本,已包 <p> 标签',
|
|
38
|
+
cc_label: cc || '(无)',
|
|
39
|
+
ready_label: raw.trim() !== '' && String(vars.entry_id || '').trim() !== '' ? '就绪' : '跳过(entry_id/message 为空)'
|
|
40
|
+
};
|
|
41
|
+
})()
|
|
42
|
+
|
|
43
|
+
# ---- 写步骤:创建评论 + 抄送(写闸门,--yes 后才执行)----
|
|
44
|
+
- id: comment
|
|
45
|
+
uses: tapd_create_comment
|
|
46
|
+
args:
|
|
47
|
+
entry_type: "${steps.prep.data.entry_type}"
|
|
48
|
+
entry_id: "${steps.prep.data.entry_id}"
|
|
49
|
+
description: "${steps.prep.data.description}"
|
|
50
|
+
cc: "${steps.prep.data.cc}"
|
|
51
|
+
where: "steps.prep.data.ready"
|
|
52
|
+
depends_on: [prep]
|
|
53
|
+
|
|
54
|
+
report:
|
|
55
|
+
format: markdown
|
|
56
|
+
template: |
|
|
57
|
+
# 变更通知回执
|
|
58
|
+
- 对象: ${steps.prep.data.entry_type} #${steps.prep.data.entry_id}
|
|
59
|
+
- 状态: ${steps.prep.data.ready_label}(写操作,措辞必须人审后 --yes 才落库)
|
|
60
|
+
- 措辞处理: ${steps.prep.data.wrapped_label}
|
|
61
|
+
- 抄送: ${steps.prep.data.cc_label}
|
|
62
|
+
- 评论 ID: ${steps.comment.data.id}
|
|
63
|
+
|
|
64
|
+
## 评论正文(预览与落库内容一致)
|
|
65
|
+
${steps.prep.data.description}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# 沟通管理 · 迭代周报数据稿(纯只读,L0 直接执行)
|
|
2
|
+
# 用法: tapd pm communication weekly [--iteration <id>]
|
|
3
|
+
# 等价: tapd pipeline run templates/pm/communication-weekly.yaml --iteration <id>
|
|
4
|
+
# 多源聚合: 迭代 stories(状态分布 + 本周新增/关闭,近 7 天 changes 近似核对)
|
|
5
|
+
# + bugs 开闭 + tasks 进度 + timesheets 工时消耗 → markdown 数据稿到 stdout。
|
|
6
|
+
# 人工补充「主观补充」段后,可选 tapd wiki create 落库(本模板不写任何对象)。
|
|
7
|
+
name: communication-weekly
|
|
8
|
+
domain: communication
|
|
9
|
+
title: 迭代周报数据稿(多源聚合到 stdout,人工补充后可选 wiki 落库)
|
|
10
|
+
write: false
|
|
11
|
+
unwrap: true
|
|
12
|
+
vars:
|
|
13
|
+
iteration_id: ""
|
|
14
|
+
steps:
|
|
15
|
+
# ---- 迭代元信息 ----
|
|
16
|
+
- id: iteration
|
|
17
|
+
uses: tapd_get_iterations
|
|
18
|
+
args: { id: "${vars.iteration_id}", fields: "id,name,startdate,enddate,status" }
|
|
19
|
+
|
|
20
|
+
# ---- 统计窗口(近 7 天,本地计算不触 API)----
|
|
21
|
+
- id: window
|
|
22
|
+
expr: |-
|
|
23
|
+
(() => {
|
|
24
|
+
const weekStart = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
|
|
25
|
+
return { today: today, week_start: weekStart };
|
|
26
|
+
})()
|
|
27
|
+
|
|
28
|
+
# ---- 需求(zzz 脏数据剔除 + 风险项剔除:label 含 risk 或名称以【风险】开头)----
|
|
29
|
+
- id: stories
|
|
30
|
+
uses: tapd_get_stories
|
|
31
|
+
args: { iteration_id: "${vars.iteration_id}", fields: "id,name,owner,status,priority_label,created,completed,label", limit: 200 }
|
|
32
|
+
filter: "!item.name.startsWith('zzz-delete-me') && !(String(item.label || '').includes('risk') || String(item.name).startsWith('【风险】'))"
|
|
33
|
+
|
|
34
|
+
# ---- 需求变更(workspace 级采样,仅用于「本周活跃需求」近似核对)----
|
|
35
|
+
- id: story_changes
|
|
36
|
+
uses: tapd_get_story_changes
|
|
37
|
+
args: { limit: 200, fields: "story_id,created,author" }
|
|
38
|
+
filter: "String(item.created || '').slice(0, 10) >= steps.window.data.week_start"
|
|
39
|
+
depends_on: [window]
|
|
40
|
+
|
|
41
|
+
# ---- 缺陷(开闭口径)----
|
|
42
|
+
- id: bugs
|
|
43
|
+
uses: tapd_get_bugs
|
|
44
|
+
args: { iteration_id: "${vars.iteration_id}", fields: "id,title,severity,current_owner,status,created,closed", limit: 200 }
|
|
45
|
+
filter: "!item.title.startsWith('zzz-delete-me')"
|
|
46
|
+
|
|
47
|
+
# ---- 任务(进度)----
|
|
48
|
+
- id: tasks
|
|
49
|
+
uses: tapd_get_tasks
|
|
50
|
+
args: { iteration_id: "${vars.iteration_id}", fields: "id,name,owner,status,progress,due", limit: 200 }
|
|
51
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
52
|
+
|
|
53
|
+
# ---- 工时(近 7 天,spentdate 时间查询;工时不分实体类型,与迭代弱耦合按窗口口径统计)----
|
|
54
|
+
- id: timesheets
|
|
55
|
+
uses: tapd_get_timesheets
|
|
56
|
+
args: { spentdate: ">= ${steps.window.data.week_start}", fields: "owner,spentdate,timespent,entity_type,entity_id", limit: 200 }
|
|
57
|
+
depends_on: [window]
|
|
58
|
+
|
|
59
|
+
# ---- 汇总(本地计算;一切比率保留分母)----
|
|
60
|
+
- id: weekly
|
|
61
|
+
expr: |-
|
|
62
|
+
(() => {
|
|
63
|
+
const day = v => String(v || '').slice(0, 10);
|
|
64
|
+
const doneStatus = s => ['done', 'closed', 'complete', 'completed'].includes(String(s || ''));
|
|
65
|
+
const ws = steps.window.data.week_start;
|
|
66
|
+
const byKey = (rows, key) => {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const r of rows) { const k = String(r[key] || '(未设置)'); out[k] = (out[k] || 0) + 1; }
|
|
69
|
+
return out;
|
|
70
|
+
};
|
|
71
|
+
const sd = steps.stories.data;
|
|
72
|
+
const doneStories = sd.filter(s => doneStatus(s.status)).length;
|
|
73
|
+
const changedIds = [...new Set(steps.story_changes.data.map(c => String(c.story_id || '')).filter(Boolean))];
|
|
74
|
+
const bd = steps.bugs.data;
|
|
75
|
+
const openBugs = bd.filter(b => !['closed', 'resolved', 'rejected'].includes(String(b.status || ''))).length;
|
|
76
|
+
const td = steps.tasks.data;
|
|
77
|
+
const doneTasks = td.filter(t => doneStatus(t.status)).length;
|
|
78
|
+
const perPerson = {};
|
|
79
|
+
for (const r of steps.timesheets.data) {
|
|
80
|
+
const who = String(r.owner || '(未设置)');
|
|
81
|
+
const e = perPerson[who] || (perPerson[who] = { owner: who, hours: 0, records: 0 });
|
|
82
|
+
e.hours += Number(r.timespent || 0);
|
|
83
|
+
e.records += 1;
|
|
84
|
+
}
|
|
85
|
+
const rows = Object.values(perPerson).sort((a, b) => b.hours - a.hours);
|
|
86
|
+
const totalHours = Math.round(rows.reduce((a, r) => a + r.hours, 0) * 100) / 100;
|
|
87
|
+
return {
|
|
88
|
+
iteration: (steps.iteration.data && steps.iteration.data[0]) || {},
|
|
89
|
+
window: steps.window.data,
|
|
90
|
+
stories: {
|
|
91
|
+
total: sd.length,
|
|
92
|
+
done: doneStories,
|
|
93
|
+
ratio: doneStories + '/' + sd.length,
|
|
94
|
+
by_status: byKey(sd, 'status'),
|
|
95
|
+
new_this_week: sd.filter(s => day(s.created) >= ws).length,
|
|
96
|
+
closed_this_week: sd.filter(s => s.completed && day(s.completed) >= ws).length,
|
|
97
|
+
changed_this_week: changedIds.length
|
|
98
|
+
},
|
|
99
|
+
bugs: {
|
|
100
|
+
total: bd.length,
|
|
101
|
+
open: openBugs,
|
|
102
|
+
ratio: (bd.length - openBugs) + '/' + bd.length,
|
|
103
|
+
new_this_week: bd.filter(b => day(b.created) >= ws).length,
|
|
104
|
+
closed_this_week: bd.filter(b => b.closed && day(b.closed) >= ws).length,
|
|
105
|
+
by_severity: byKey(bd, 'severity')
|
|
106
|
+
},
|
|
107
|
+
tasks: {
|
|
108
|
+
total: td.length,
|
|
109
|
+
done: doneTasks,
|
|
110
|
+
ratio: doneTasks + '/' + td.length,
|
|
111
|
+
not_started: td.filter(t => !doneStatus(t.status) && Number(t.progress || 0) === 0).length,
|
|
112
|
+
overdue: td.filter(t => !doneStatus(t.status) && t.due && day(t.due) < today).length,
|
|
113
|
+
avg_progress: td.length ? Math.round(td.reduce((a, t) => a + Number(t.progress || 0), 0) / td.length) : 0
|
|
114
|
+
},
|
|
115
|
+
timesheets: { hours: totalHours, people: rows.length, rows: rows }
|
|
116
|
+
};
|
|
117
|
+
})()
|
|
118
|
+
depends_on: [iteration, window, stories, story_changes, bugs, tasks, timesheets]
|
|
119
|
+
|
|
120
|
+
report:
|
|
121
|
+
format: markdown
|
|
122
|
+
template: |
|
|
123
|
+
# 迭代周报(数据稿)
|
|
124
|
+
> 本稿由 `tapd pm communication weekly` 只读聚合生成,为**数据稿,人工补充后可选 wiki 落库**(如 `tapd wiki create`)。
|
|
125
|
+
|
|
126
|
+
- 迭代: ${steps.weekly.data.iteration.name}(${steps.weekly.data.iteration.startdate} ~ ${steps.weekly.data.iteration.enddate})
|
|
127
|
+
- 统计窗口: ${steps.weekly.data.window.week_start} ~ ${steps.weekly.data.window.today}(近 7 天)
|
|
128
|
+
|
|
129
|
+
## 需求
|
|
130
|
+
- 总数 ${steps.weekly.data.stories.total},完成度 ${steps.weekly.data.stories.ratio}
|
|
131
|
+
- 状态分布: ${steps.weekly.data.stories.by_status | json}
|
|
132
|
+
- 本周新增 ${steps.weekly.data.stories.new_this_week} / 本周关闭 ${steps.weekly.data.stories.closed_this_week}(changes 近似核对: ${steps.weekly.data.stories.changed_this_week} 条需求在窗口内有变更记录)
|
|
133
|
+
|
|
134
|
+
## 缺陷
|
|
135
|
+
- 总数 ${steps.weekly.data.bugs.total},已关闭 ${steps.weekly.data.bugs.ratio},现存未关闭 ${steps.weekly.data.bugs.open}
|
|
136
|
+
- 本周新增 ${steps.weekly.data.bugs.new_this_week} / 本周关闭 ${steps.weekly.data.bugs.closed_this_week}
|
|
137
|
+
- 严重度分布: ${steps.weekly.data.bugs.by_severity | json}
|
|
138
|
+
|
|
139
|
+
## 任务
|
|
140
|
+
- 总数 ${steps.weekly.data.tasks.total},完成 ${steps.weekly.data.tasks.ratio},未启动 ${steps.weekly.data.tasks.not_started},逾期未完成 ${steps.weekly.data.tasks.overdue}
|
|
141
|
+
- 平均进度 ${steps.weekly.data.tasks.avg_progress}%
|
|
142
|
+
|
|
143
|
+
## 工时(近 7 天)
|
|
144
|
+
- 合计 ${steps.weekly.data.timesheets.hours} 小时 / ${steps.weekly.data.timesheets.people} 人
|
|
145
|
+
| 成员 | 工时(h) | 记录数 |
|
|
146
|
+
|------|---------|--------|
|
|
147
|
+
${steps.weekly.data.timesheets.rows | rows owner,hours,records}
|
|
148
|
+
|
|
149
|
+
## 口径脚注
|
|
150
|
+
- 统计范围: 迭代 ${vars.iteration_id}(需求/缺陷/任务按 iteration_id 归属;工时按 spentdate 落入近 7 天窗口,跨迭代工时不剔重)
|
|
151
|
+
- 过滤项: 标题 zzz-delete-me* 数据已剔除;范围类度量剔除风险项(label 含 risk 或名称以【风险】开头)
|
|
152
|
+
- 数据时间: ${steps.weekly.data.window.today};「本周」= ${steps.weekly.data.window.week_start} 起(7 个自然日)
|
|
153
|
+
- 完成度定义: 需求 status ∈ {done, closed, complete, completed} 的数量 / 需求总数,分母保留不省略
|
|
154
|
+
- 局限: story_changes 为 workspace 级近 200 条采样,仅作「本周活跃需求」近似核对;周新增/关闭以需求 created/completed 字段为准
|
|
155
|
+
|
|
156
|
+
## 主观补充
|
|
157
|
+
(以下由人工填写后再落库:本周亮点 / 风险与求助 / 下周计划)
|
|
158
|
+
- 亮点:
|
|
159
|
+
- 风险与求助:
|
|
160
|
+
- 下周计划:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 工时执行报告(软考第11章 成本管理,纯只读 L0,类比 EVM)
|
|
2
|
+
# 数据源:迭代 tasks(effort/effort_completed/remain/exceed/owner)+ tapd_get_timesheets
|
|
3
|
+
# (spentdate 按迭代区间时间查询,校验填报完整性)
|
|
4
|
+
# 用法: tapd pm cost report [--iteration <id>] [--dry-run]
|
|
5
|
+
# 可用 --input '{"date_from":"2026-09-01","date_to":"2026-09-11"}' 覆盖填报窗口
|
|
6
|
+
name: cost-report
|
|
7
|
+
domain: cost
|
|
8
|
+
title: 工时执行报告(effort/exceed 聚合 + timesheets 完整性校验,类比 EVM)
|
|
9
|
+
write: false
|
|
10
|
+
unwrap: true
|
|
11
|
+
vars:
|
|
12
|
+
iteration_id: ""
|
|
13
|
+
date_from: ""
|
|
14
|
+
date_to: ""
|
|
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
|
+
|
|
22
|
+
- id: tasks
|
|
23
|
+
uses: tapd_get_tasks
|
|
24
|
+
args:
|
|
25
|
+
iteration_id: "${vars.iteration_id}"
|
|
26
|
+
fields: "id,name,owner,status,effort,effort_completed,remain,exceed"
|
|
27
|
+
limit: 200
|
|
28
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
29
|
+
|
|
30
|
+
- id: window
|
|
31
|
+
expr: "(() => { const it = (steps.iteration.data ?? [])[0] ?? {}; const vf = String(vars.date_from ?? ''), vt = String(vars.date_to ?? ''); const from = vf !== '' ? vf : (String(it.startdate ?? '').slice(0, 10) || today); const to = vt !== '' ? vt : (String(it.enddate ?? '').slice(0, 10) || today); return { from: from, to: to, range: from + '~' + to }; })()"
|
|
32
|
+
depends_on: [iteration]
|
|
33
|
+
|
|
34
|
+
- id: timesheets
|
|
35
|
+
uses: tapd_get_timesheets
|
|
36
|
+
args:
|
|
37
|
+
spentdate: "${steps.window.data.range}"
|
|
38
|
+
fields: "entity_type,entity_id,owner,timespent,spentdate"
|
|
39
|
+
limit: 200
|
|
40
|
+
depends_on: [window]
|
|
41
|
+
|
|
42
|
+
- id: cost
|
|
43
|
+
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 ownerOf = v => { const s = String(v ?? '').trim(); return s === '' ? '(未分派)' : s; }; const r2 = n => Math.round(n * 100) / 100; const t = arr(steps.tasks.data); const ts = arr(steps.timesheets.data); const byOwner = {}; const ensure = o => { if (!byOwner[o]) byOwner[o] = { owner: o, tasks: 0, effort: 0, effortCompleted: 0, remain: 0, exceed: 0, timespent: 0 }; return byOwner[o]; }; let budget = 0, used = 0, remain = 0, exceed = 0; for (const k of t) { const r = ensure(ownerOf(k.owner)); r.tasks++; r.effort += num(k.effort); r.effortCompleted += num(k.effort_completed); r.remain += num(k.remain); r.exceed += num(k.exceed); budget += num(k.effort); used += num(k.effort_completed); remain += num(k.remain); exceed += num(k.exceed); } for (const w of ts) { ensure(ownerOf(w.owner)).timespent += num(w.timespent); } const ownerRows = Object.values(byOwner).map(r => ({ owner: r.owner, tasks: r.tasks, effort: r2(r.effort), effortCompleted: r2(r.effortCompleted), remain: r2(r.remain), exceed: r2(r.exceed), timespent: r2(r.timespent), rate: r.effort > 0 ? r2(r.effortCompleted) + '/' + r2(r.effort) : '0/0' })).sort((a, b) => b.effort - a.effort); const ac = ts.reduce((a, w) => a + num(w.timespent), 0); const filledEntities = new Set(ts.filter(w => String(w.entity_type) === 'task').map(w => String(w.entity_id))); const noSheet = t.filter(k => num(k.effort) > 0 && !filledEntities.has(String(k.id))); const missingOwners = ownerRows.filter(r => r.effort > 0 && r.timespent === 0).map(r => ({ owner: r.owner, effort: r.effort, tasks: r.tasks })); const totalsRows = [{ label: '预算 BAC = Σeffort', hours: r2(budget) }, { label: '已耗 EV = Σeffort_completed', hours: r2(used) }, { label: '剩余 = Σremain', hours: r2(remain) }, { label: '超支 = Σexceed', hours: r2(exceed) }, { label: '填报 AC = Σtimespent(区间内)', hours: r2(ac) }, { label: '偏差 CV = EV - AC', hours: r2(used - ac) }, { label: 'CPI = EV/AC', hours: ac > 0 ? r2(used / ac) + '(' + r2(used) + '/' + r2(ac) + ')' : 'n/a(分母为 0)' }]; return { totals: { budget: r2(budget), used: r2(used), remain: r2(remain), exceed: r2(exceed), ac: r2(ac), cv: r2(used - ac), window: steps.window.data, taskCount: t.length, sheetCount: ts.length, missingOwnerCount: missingOwners.length, missingTaskCount: noSheet.length }, totalsRows: totalsRows, ownerRows: ownerRows, missingOwners: missingOwners, missingTasks: noSheet.slice(0, 20).map(k => ({ id: String(k.id), name: k.name, owner: ownerOf(k.owner), effort: num(k.effort) })) }; })()"
|
|
44
|
+
depends_on: [iteration, tasks, window, timesheets]
|
|
45
|
+
|
|
46
|
+
report:
|
|
47
|
+
format: markdown
|
|
48
|
+
template: |
|
|
49
|
+
# 工时执行报告 — ${steps.iteration.data.0.name}(${steps.iteration.data.0.startdate} ~ ${steps.iteration.data.0.enddate})
|
|
50
|
+
|
|
51
|
+
- 任务 ${steps.cost.data.totals.taskCount} 个(已剔除 zzz-delete-me* 测试数据);工时填报记录 ${steps.cost.data.totals.sheetCount} 条,窗口 ${steps.cost.data.totals.window.from} ~ ${steps.cost.data.totals.window.to}
|
|
52
|
+
|
|
53
|
+
## 总量(EVM 类比)
|
|
54
|
+
| 指标 | 工时(h) |
|
|
55
|
+
|---|---|
|
|
56
|
+
${steps.cost.data.totalsRows | rows label,hours}
|
|
57
|
+
|
|
58
|
+
## 按 owner 分解
|
|
59
|
+
| 处理人 | 任务数 | 预算(h) | 已耗 EV(h) | 剩余(h) | 超支(h) | 填报 AC(h) | 完成率 EV/预算 |
|
|
60
|
+
|---|---|---|---|---|---|---|---|
|
|
61
|
+
${steps.cost.data.ownerRows | rows owner,tasks,effort,effortCompleted,remain,exceed,timespent,rate}
|
|
62
|
+
|
|
63
|
+
## 填报缺失名单(owner 级:有预算任务但区间零填报,共 ${steps.cost.data.totals.missingOwnerCount} 人)
|
|
64
|
+
| 处理人 | 名下任务预算(h) | 任务数 |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
${steps.cost.data.missingOwners | rows owner,effort,tasks}
|
|
67
|
+
|
|
68
|
+
## 任务级缺失(effort>0 且窗口内无该任务填报,共 ${steps.cost.data.totals.missingTaskCount} 个,至多列 20)
|
|
69
|
+
| id | 任务 | 处理人 | 预算(h) |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
${steps.cost.data.missingTasks | rows id,name,owner,effort}
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
口径:预算=Σ任务 effort;已耗(EV)=Σ effort_completed;剩余=Σ remain;超支=Σ exceed;填报(AC)=区间内 timesheets 的 timespent 合计(spentdate 用 TAPD 时间区间查询「起~止」,limit 200 超限截断不翻页);完成率与 CPI 均保留分母(如 9/12);owner 级缺失 = 该人名下任务 effort>0 但区间零填报记录;任务级缺失 = 任务 effort>0 且区间内无 entity_type=task、entity_id=该任务的填报记录(以任务表 effort 口径为准,不与 timesheet 交叉折算)。
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# 结项检查与关闭(写场景)—— 整合管理(软考高项第4版 第8章)
|
|
2
|
+
# 场景: tapd pm integration close
|
|
3
|
+
# ⚠ 请先读 dry-run 报告确认无未完成项再加 --yes(不加 --yes 时本模板被写闸门拦截,只出检查报告预览)
|
|
4
|
+
# 编排: 只读扫描(迭代内未完成需求/任务清单)→ 结项检查报告 → tapd_update_iteration(status=done)
|
|
5
|
+
# 口径: 完成态 = resolved/closed/done 或已填 completed;任务完成态 = done;列表过滤 zzz-delete-me*
|
|
6
|
+
name: integration-close
|
|
7
|
+
domain: integration
|
|
8
|
+
title: 结项检查与关闭(写场景:请先读 dry-run 报告确认无未完成项再加 --yes,软考第8章)
|
|
9
|
+
write: true
|
|
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
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
20
|
+
|
|
21
|
+
# ---- 只读扫描段(波次 1,并发)----
|
|
22
|
+
- id: stories
|
|
23
|
+
uses: tapd_get_stories
|
|
24
|
+
args:
|
|
25
|
+
iteration_id: "${vars.iteration_id}"
|
|
26
|
+
limit: 200
|
|
27
|
+
fields: "id,name,owner,status,completed"
|
|
28
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
29
|
+
|
|
30
|
+
- id: tasks
|
|
31
|
+
uses: tapd_get_tasks
|
|
32
|
+
args:
|
|
33
|
+
iteration_id: "${vars.iteration_id}"
|
|
34
|
+
limit: 200
|
|
35
|
+
fields: "id,name,owner,status,due"
|
|
36
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
37
|
+
|
|
38
|
+
- id: scan
|
|
39
|
+
expr: >-
|
|
40
|
+
(() => {
|
|
41
|
+
const stories = steps.stories.data || [];
|
|
42
|
+
const tasks = steps.tasks.data || [];
|
|
43
|
+
const it = (steps.iteration.data && steps.iteration.data[0]) || {};
|
|
44
|
+
const DONE = ['resolved', 'closed', 'done'];
|
|
45
|
+
const isDoneStory = x => DONE.indexOf(String(x.status || '').toLowerCase()) >= 0 || Boolean(x.completed);
|
|
46
|
+
const openStories = stories.filter(x => !isDoneStory(x));
|
|
47
|
+
const openTasks = tasks.filter(x => String(x.status || '').toLowerCase() !== 'done');
|
|
48
|
+
const blockers = openStories.length + openTasks.length;
|
|
49
|
+
return {
|
|
50
|
+
itName: it.name || '(未找到迭代,请检查 --iteration)',
|
|
51
|
+
itRange: (it.startdate || '?') + ' ~ ' + (it.enddate || '?'),
|
|
52
|
+
storyTotal: stories.length,
|
|
53
|
+
openStoryCount: openStories.length,
|
|
54
|
+
openStoryRows: openStories.length > 0
|
|
55
|
+
? openStories.map(x => ({ id: x.id, name: x.name, owner: x.owner, status: x.status }))
|
|
56
|
+
: [{ id: '-', name: '(无未完成需求)', owner: '-', status: '-' }],
|
|
57
|
+
taskTotal: tasks.length,
|
|
58
|
+
openTaskCount: openTasks.length,
|
|
59
|
+
openTaskRows: openTasks.length > 0
|
|
60
|
+
? openTasks.map(x => ({ id: x.id, name: x.name, owner: x.owner, status: x.status }))
|
|
61
|
+
: [{ id: '-', name: '(无未完成任务)', owner: '-', status: '-' }],
|
|
62
|
+
blockers: blockers,
|
|
63
|
+
verdict: blockers === 0 ? '无未完成项,可结项' : '存在 ' + blockers + ' 个未完成项——请勿加 --yes,先处理或移交',
|
|
64
|
+
};
|
|
65
|
+
})()
|
|
66
|
+
depends_on: [iteration, stories, tasks]
|
|
67
|
+
|
|
68
|
+
# ---- 写步骤(双保险:无 --yes 时被写闸门拦截;--yes 后仍由扫描结果门控——
|
|
69
|
+
# 还有未完成项(blockers>0)时 where 为假,写步骤跳过不落库)----
|
|
70
|
+
- id: close
|
|
71
|
+
uses: tapd_update_iteration
|
|
72
|
+
args:
|
|
73
|
+
id: "${vars.iteration_id}"
|
|
74
|
+
status: done
|
|
75
|
+
where: "steps.scan.data.blockers === 0"
|
|
76
|
+
depends_on: [scan]
|
|
77
|
+
|
|
78
|
+
- id: close_ref
|
|
79
|
+
expr: >-
|
|
80
|
+
(() => {
|
|
81
|
+
const r = steps.close.data || {};
|
|
82
|
+
const inner = r.Iteration || r.iteration || r;
|
|
83
|
+
return {
|
|
84
|
+
id: String(inner.id || vars.iteration_id),
|
|
85
|
+
closeLine: '已执行,迭代 ' + (inner.id || vars.iteration_id) + ' 现状态: ' + (inner.status || 'done'),
|
|
86
|
+
};
|
|
87
|
+
})()
|
|
88
|
+
depends_on: [close]
|
|
89
|
+
|
|
90
|
+
report:
|
|
91
|
+
format: markdown
|
|
92
|
+
template: |
|
|
93
|
+
# 结项检查报告 — ${steps.scan.data.itName}
|
|
94
|
+
|
|
95
|
+
- 迭代: ${steps.scan.data.itRange}(id=${vars.iteration_id})
|
|
96
|
+
- 扫描结论: ${steps.scan.data.verdict}
|
|
97
|
+
- 需求: 共 ${steps.scan.data.storyTotal} 项,未完成 ${steps.scan.data.openStoryCount}
|
|
98
|
+
- 任务: 共 ${steps.scan.data.taskTotal} 个,未完成 ${steps.scan.data.openTaskCount}
|
|
99
|
+
|
|
100
|
+
> ⚠ 本场景含写步骤 tapd_update_iteration(status=done)。请先读 dry-run 计划与扫描结论
|
|
101
|
+
> 确认无未完成项再加 --yes;双保险:不加 --yes 时写闸门拦截(仅计划预览),
|
|
102
|
+
> 加了 --yes 但扫描仍有未完成项时,写步骤按 where 门控自动跳过,不落库。
|
|
103
|
+
|
|
104
|
+
## 未完成需求(${steps.scan.data.openStoryCount} 项)
|
|
105
|
+
|
|
106
|
+
| id | 名称 | 处理人 | 状态 |
|
|
107
|
+
| ---- | ---- | ---- | ---- |
|
|
108
|
+
${steps.scan.data.openStoryRows | rows id,name,owner,status}
|
|
109
|
+
|
|
110
|
+
## 未完成任务(${steps.scan.data.openTaskCount} 个)
|
|
111
|
+
|
|
112
|
+
| id | 名称 | 处理人 | 状态 |
|
|
113
|
+
| ---- | ---- | ---- | ---- |
|
|
114
|
+
${steps.scan.data.openTaskRows | rows id,name,owner,status}
|
|
115
|
+
|
|
116
|
+
## 关闭结果
|
|
117
|
+
|
|
118
|
+
- tapd_update_iteration(id=${vars.iteration_id}, status=done):${steps.close_ref.data.closeLine}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# 迭代健康盘(纯只读,L0 直接执行)—— 整合管理(软考高项第4版 第8章)
|
|
2
|
+
# 场景: tapd pm integration health
|
|
3
|
+
# ≡ tapd pipeline run templates/pm/integration-health.yaml --iteration <id>
|
|
4
|
+
# 口径:
|
|
5
|
+
# 时间消耗% = (今天-开始)/(结束-开始),截断到 [0,100]
|
|
6
|
+
# 完成度% = 已完成需求占比,分母剔除风险项(label 含 risk 或名称以【风险】开头)
|
|
7
|
+
# 完成态 = resolved/closed/done(resolved+end,兼容常见工作流别名)
|
|
8
|
+
# 红旗 = 时间-完成偏差>20% / 逾期任务 / 未解缺陷 / 迭代到期仍有未完成需求
|
|
9
|
+
# 一切比率保留分母(如 8/15),迭代/需求/任务/缺陷列表均过滤 zzz-delete-me*
|
|
10
|
+
name: integration-health
|
|
11
|
+
domain: integration
|
|
12
|
+
title: 迭代健康盘(时间%vs完成度%对比 + 红旗清单,软考第8章)
|
|
13
|
+
write: false
|
|
14
|
+
unwrap: true
|
|
15
|
+
vars:
|
|
16
|
+
iteration_id: ""
|
|
17
|
+
steps:
|
|
18
|
+
- id: iteration
|
|
19
|
+
uses: tapd_get_iterations
|
|
20
|
+
args:
|
|
21
|
+
id: "${vars.iteration_id}"
|
|
22
|
+
fields: "id,name,startdate,enddate,status"
|
|
23
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
24
|
+
|
|
25
|
+
- id: stories
|
|
26
|
+
uses: tapd_get_stories
|
|
27
|
+
args:
|
|
28
|
+
iteration_id: "${vars.iteration_id}"
|
|
29
|
+
limit: 200
|
|
30
|
+
fields: "id,name,owner,status,priority_label,label,completed"
|
|
31
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
32
|
+
|
|
33
|
+
- id: tasks
|
|
34
|
+
uses: tapd_get_tasks
|
|
35
|
+
args:
|
|
36
|
+
iteration_id: "${vars.iteration_id}"
|
|
37
|
+
limit: 200
|
|
38
|
+
fields: "id,name,owner,status,due,progress"
|
|
39
|
+
filter: "!item.name.startsWith('zzz-delete-me')"
|
|
40
|
+
|
|
41
|
+
- id: bugs
|
|
42
|
+
uses: tapd_get_bugs
|
|
43
|
+
args:
|
|
44
|
+
iteration_id: "${vars.iteration_id}"
|
|
45
|
+
limit: 200
|
|
46
|
+
fields: "id,title,severity,current_owner,status"
|
|
47
|
+
filter: "!item.title.startsWith('zzz-delete-me')"
|
|
48
|
+
|
|
49
|
+
# 聚合:本地计算,不触 API。dayNum 为纯 JS 民用日序数(沙箱无 Date,只许 Math/String)
|
|
50
|
+
- id: health
|
|
51
|
+
expr: >-
|
|
52
|
+
(() => {
|
|
53
|
+
const stories = steps.stories.data || [];
|
|
54
|
+
const tasks = steps.tasks.data || [];
|
|
55
|
+
const bugs = steps.bugs.data || [];
|
|
56
|
+
const it = (steps.iteration.data && steps.iteration.data[0]) || {};
|
|
57
|
+
const dayNum = s => {
|
|
58
|
+
const p = String(s || '').split('-').map(Number);
|
|
59
|
+
if (p.length !== 3 || p.some(x => Number.isNaN(x))) return null;
|
|
60
|
+
const m = p[1];
|
|
61
|
+
const d = p[2];
|
|
62
|
+
const yy = m <= 2 ? p[0] - 1 : p[0];
|
|
63
|
+
const era = Math.floor(yy / 400);
|
|
64
|
+
const yoe = yy - era * 400;
|
|
65
|
+
const mp = (m + 9) % 12;
|
|
66
|
+
const doy = Math.floor((153 * mp + 2) / 5) + d - 1;
|
|
67
|
+
const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
|
|
68
|
+
return era * 146097 + doe - 719468;
|
|
69
|
+
};
|
|
70
|
+
const t = dayNum(today);
|
|
71
|
+
const s0 = dayNum(it.startdate);
|
|
72
|
+
const e0 = dayNum(it.enddate);
|
|
73
|
+
let timePct = null;
|
|
74
|
+
if (s0 !== null && e0 !== null && e0 > s0) {
|
|
75
|
+
timePct = Math.round(Math.min(Math.max((t - s0) / (e0 - s0), 0), 1) * 100);
|
|
76
|
+
}
|
|
77
|
+
const DONE = ['resolved', 'closed', 'done'];
|
|
78
|
+
const BUG_CLOSED = ['resolved', 'verified', 'closed', 'rejected'];
|
|
79
|
+
const isRisk = x => String(x.label || '').indexOf('risk') >= 0 || String(x.name || '').indexOf('【风险】') === 0;
|
|
80
|
+
const isDone = x => DONE.indexOf(String(x.status || '').toLowerCase()) >= 0 || Boolean(x.completed);
|
|
81
|
+
const scope = stories.filter(x => !isRisk(x));
|
|
82
|
+
const doneList = scope.filter(isDone);
|
|
83
|
+
const donePct = scope.length > 0 ? Math.round(doneList.length / scope.length * 100) : null;
|
|
84
|
+
const overdue = tasks.filter(x => x.due && String(x.due).slice(0, 10) < today && String(x.status || '').toLowerCase() !== 'done');
|
|
85
|
+
const openBugs = bugs.filter(x => BUG_CLOSED.indexOf(String(x.status || '').toLowerCase()) < 0);
|
|
86
|
+
const notDone = scope.filter(x => !isDone(x));
|
|
87
|
+
const flags = [];
|
|
88
|
+
if (timePct !== null && donePct !== null && timePct - donePct > 20) {
|
|
89
|
+
flags.push({ item: '进度滞后', detail: '时间消耗 ' + timePct + '% 高于完成度 ' + donePct + '%(偏差 ' + (timePct - donePct) + 'pp,阈值 20%)' });
|
|
90
|
+
}
|
|
91
|
+
if (overdue.length > 0) {
|
|
92
|
+
flags.push({ item: '逾期任务', detail: overdue.length + '/' + tasks.length + ' 个任务逾期未完(如 #' + overdue[0].id + ' ' + overdue[0].name + ')' });
|
|
93
|
+
}
|
|
94
|
+
if (openBugs.length > 0) {
|
|
95
|
+
flags.push({ item: '未解缺陷', detail: '未解决缺陷 ' + openBugs.length + '/' + bugs.length + ' 个(sev1=' + openBugs.filter(b => String(b.severity) === '1').length + ')' });
|
|
96
|
+
}
|
|
97
|
+
if (e0 !== null && t > e0 && notDone.length > 0) {
|
|
98
|
+
flags.push({ item: '到期未完', detail: '迭代已于 ' + it.enddate + ' 到期,仍有 ' + notDone.length + ' 个需求未完成' });
|
|
99
|
+
}
|
|
100
|
+
let timeDisplay = '迭代缺少起止日期,无法计算';
|
|
101
|
+
if (timePct !== null) {
|
|
102
|
+
const tail = e0 !== null && t > e0 ? '已超期 ' + (t - e0) + ' 天' : '距结束还剩 ' + (e0 - t) + ' 天';
|
|
103
|
+
timeDisplay = timePct + '%(' + tail + ')';
|
|
104
|
+
}
|
|
105
|
+
const riskCount = stories.length - scope.length;
|
|
106
|
+
const realFlags = flags.slice();
|
|
107
|
+
if (realFlags.length === 0) flags.push({ item: '-', detail: '无红旗(时间/任务/缺陷/到期四路扫描均通过)' });
|
|
108
|
+
return {
|
|
109
|
+
itName: it.name || '(未找到迭代,请检查 --iteration)',
|
|
110
|
+
itRange: (it.startdate || '?') + ' ~ ' + (it.enddate || '?'),
|
|
111
|
+
itStatus: it.status || '?',
|
|
112
|
+
today: today,
|
|
113
|
+
statusLabel: realFlags.length === 0 ? '绿(健康)' : '红(' + realFlags.length + ' 项红旗)',
|
|
114
|
+
timeDisplay: timeDisplay,
|
|
115
|
+
doneDisplay: donePct === null ? '无有效需求范围' : doneList.length + '/' + scope.length + '(' + donePct + '%)',
|
|
116
|
+
riskNote: '分母剔除风险项 ' + riskCount + ' 个:label 含 risk 或名称以【风险】开头',
|
|
117
|
+
storyLine: stories.length + ' 项(范围口径 ' + scope.length + ')',
|
|
118
|
+
taskLine: tasks.length + ' 个(逾期 ' + overdue.length + ')',
|
|
119
|
+
bugLine: bugs.length + ' 个(未解决 ' + openBugs.length + ')',
|
|
120
|
+
flagSummary: realFlags.length === 0 ? '无' : realFlags.length + ' 项,见下表',
|
|
121
|
+
flags: flags,
|
|
122
|
+
notDoneCount: notDone.length,
|
|
123
|
+
notDoneRows: notDone.length > 0
|
|
124
|
+
? notDone.map(x => ({ id: x.id, name: x.name, owner: x.owner, status: x.status }))
|
|
125
|
+
: [{ id: '-', name: '(范围内需求已全部完成)', owner: '-', status: '-' }],
|
|
126
|
+
};
|
|
127
|
+
})()
|
|
128
|
+
depends_on: [iteration, stories, tasks, bugs]
|
|
129
|
+
|
|
130
|
+
report:
|
|
131
|
+
format: markdown
|
|
132
|
+
template: |
|
|
133
|
+
# 迭代健康盘 — ${steps.health.data.itName}
|
|
134
|
+
|
|
135
|
+
- 迭代: ${steps.health.data.itRange}(status=${steps.health.data.itStatus})
|
|
136
|
+
- 报告日期: ${steps.health.data.today}|总体状态: ${steps.health.data.statusLabel}
|
|
137
|
+
- 时间消耗: ${steps.health.data.timeDisplay}
|
|
138
|
+
- 完成度: ${steps.health.data.doneDisplay}(${steps.health.data.riskNote})
|
|
139
|
+
- 需求: ${steps.health.data.storyLine}|任务: ${steps.health.data.taskLine}|缺陷: ${steps.health.data.bugLine}
|
|
140
|
+
- 红旗: ${steps.health.data.flagSummary}
|
|
141
|
+
|
|
142
|
+
## 红旗清单
|
|
143
|
+
|
|
144
|
+
| 类别 | 说明 |
|
|
145
|
+
| ---- | ---- |
|
|
146
|
+
${steps.health.data.flags | rows item,detail}
|
|
147
|
+
|
|
148
|
+
## 未完成需求(范围口径 ${steps.health.data.notDoneCount} 项)
|
|
149
|
+
|
|
150
|
+
| id | 名称 | 处理人 | 状态 |
|
|
151
|
+
| ---- | ---- | ---- | ---- |
|
|
152
|
+
${steps.health.data.notDoneRows | rows id,name,owner,status}
|
|
153
|
+
|
|
154
|
+
> 口径脚注(对齐 tapd-pm skill 11-performance 剧本A):完成度只统计范围口径
|
|
155
|
+
> (剔除 label 含 risk / 名称【风险】前缀);完成态 = resolved+end;比率均保留分母。
|