scrum-skills 0.3.0 → 0.3.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 (33) hide show
  1. package/.agents/skills/scrum-planning/references/schema.md +82 -4
  2. package/.agents/skills/scrum-report/SKILL.md +4 -1
  3. package/.agents/skills/scrum-sprint/SKILL.md +1 -1
  4. package/.agents/skills/scrum-tdd/SKILL.md +19 -5
  5. package/README.md +11 -7
  6. package/assets/board/200.html +1 -1
  7. package/assets/board/404.html +1 -1
  8. package/assets/board/_nuxt/BbwJq5pb.js +1 -0
  9. package/assets/board/_nuxt/{CkF353aX.js → DkI1Qxpp.js} +49 -49
  10. package/assets/board/_nuxt/S08nFESZ.js +3 -0
  11. package/assets/board/_nuxt/builds/latest.json +1 -1
  12. package/assets/board/_nuxt/builds/meta/8e23aec9-5b67-4569-b8bf-d4f07e50ffdf.json +1 -0
  13. package/assets/board/_nuxt/{entry.Dx3CNA--.css → entry.CKHePurH.css} +1 -1
  14. package/assets/board/_nuxt/last-change.1Su4GrCN.css +1 -0
  15. package/assets/board/_nuxt/tests.dXlUX16A.css +1 -0
  16. package/assets/board/burndown/index.html +1 -1
  17. package/assets/board/epics/index.html +1 -1
  18. package/assets/board/gantt/index.html +1 -1
  19. package/assets/board/index.html +1 -1
  20. package/assets/board/last-change/index.html +1 -1
  21. package/assets/board/milestones/index.html +1 -1
  22. package/assets/board/tests/index.html +1 -0
  23. package/bin/scrum-skills.mjs +4 -2
  24. package/package.json +1 -1
  25. package/templates/AGENTS.md +12 -7
  26. package/tools/agile-tools.mjs +9 -3
  27. package/tools/ai.mjs +5 -5
  28. package/tools/git-commit.mjs +216 -0
  29. package/tools/projects.mjs +23 -2
  30. package/tools/validate.mjs +161 -0
  31. package/assets/board/_nuxt/DrprggWq.js +0 -3
  32. package/assets/board/_nuxt/builds/meta/d7f0931f-32b3-4c23-b05f-7cbf5851b226.json +0 -1
  33. package/assets/board/_nuxt/last-change.8FndoiOt.css +0 -1
@@ -1,15 +1,16 @@
1
- # 敏捷数据 Schema(v3 · 三文件结构)
1
+ # 敏捷数据 Schema(v3 · 三常驻 + 两可选)
2
2
 
3
- `agile/` 下**三个常驻数据文件 + 一个可选的交付说明**,全部 UTF-8 JSON、2 空格缩进、末尾换行。
3
+ `agile/` 下**三个常驻数据文件 + 两个可选文件**(测试资产、交付说明),全部 UTF-8 JSON、2 空格缩进、末尾换行。
4
4
 
5
5
  | 文件 | 装什么 |
6
6
  |---|---|
7
7
  | `agile/backlog.json` | 史诗 epics(含量化目标 keyResults)+ 用户故事 stories(内联 tasks) |
8
8
  | `agile/sprints.json` | 迭代 sprints + 里程碑 milestones(MVP 节点是里程碑的一种) |
9
9
  | `agile/journal.json` | TDD 实施日志 journals(一个 task 一条,内联 entries) |
10
+ | `agile/tests.json` | 测试资产:suites(层 × 级别 → cases)+ runs(执行记录与代码覆盖率),见第 5 节 |
10
11
  | `agile/last-change.json` | 最近一次交付的汇报(可选;已提交才覆盖,未提交则合并,见第 4 节) |
11
12
 
12
- > 三个文件都必须带 `"schemaVersion": 3`。v2 及更早的散文件结构不再兼容,校验器发现 `agile/backlog/epics/`、`agile/backlog/stories/`、`agile/sprints/`、`agile/journal/` 里还有 JSON 会直接报错。
13
+ > 所有数据文件都必须带 `"schemaVersion": 3`(含可选的 `tests.json` / `last-change.json`)。v2 及更早的散文件结构不再兼容,校验器发现 `agile/backlog/epics/`、`agile/backlog/stories/`、`agile/sprints/`、`agile/journal/` 里还有 JSON 会直接报错。
13
14
 
14
15
  ## 1. backlog.json
15
16
 
@@ -234,6 +235,83 @@ node tools/last-change.mjs write <记录.json> --replace # 确认旧记录不
234
235
  - `verification[].result` 与 `scenarios[].expect` 是「验收不是口号」的凭据:场景必须写清步骤与验收标准,验证结果只认 `pass/fail/partial/skipped`。
235
236
  - 校验器会查:`schemaVersion`、`at` 格式、`kind`/`action`/`result` 枚举、`changes[].id` 是否真实存在、`scenarios[].steps` 与 `expect` 是否齐备、`access.urls[].url` 是否为 `http(s)` 完整地址。缺 `title`、`what`、`expect`、`step` 报错;缺 `changes[].title`、场景 id、url label 只给 WARN。
236
237
 
238
+ ## 5. tests.json(测试资产与覆盖率)
239
+
240
+ `agile/tests.json` 是**可选**文件:不存在时校验器不报错、看板 `/tests` 页显示空状态。它由 Coding Agent 在**每个 task 推到 `done`、story 送进 `review` 之前**维护(见 `scrum-tdd` 的交工汇报第 1 步),不是事后补的报表。
241
+
242
+ ```json
243
+ {
244
+ "schemaVersion": 3,
245
+ "suites": [
246
+ {
247
+ "id": "TS-BE-01",
248
+ "name": "登录接口",
249
+ "layer": "frontend | backend | e2e",
250
+ "level": "unit | integration | e2e",
251
+ "runner": "vitest",
252
+ "files": ["tests/auth/login.spec.ts"],
253
+ "cases": [
254
+ {
255
+ "id": "TC-BE-001",
256
+ "title": "正确密码登录返回 200 并下发 httpOnly cookie",
257
+ "automated": true,
258
+ "taskId": "S-001-T1",
259
+ "covers": ["S-001-A1"],
260
+ "expect": "200;set-cookie 含 sid;HttpOnly",
261
+ "code": "tests/auth/login.spec.ts::正确密码登录",
262
+ "status": "pass | fail | skipped | blocked | not-run",
263
+ "lastRun": "2026-10-29",
264
+ "durationMs": 42,
265
+ "note": "失败原因写在这里"
266
+ }
267
+ ]
268
+ }
269
+ ],
270
+ "runs": [
271
+ {
272
+ "id": "RUN-20261029-2110",
273
+ "at": "2026-10-29 21:10:00",
274
+ "env": "local",
275
+ "command": "pnpm test",
276
+ "totals": { "total": 159, "pass": 158, "fail": 1, "skipped": 0, "durationMs": 24300 },
277
+ "failedCases": ["TC-BE-001"],
278
+ "coverage": {
279
+ "frontend": { "tool": "vitest", "lines": 72.4, "branches": 61.5, "functions": 66.0, "report": "coverage/frontend/lcov.info" },
280
+ "backend": { "tool": "vitest", "lines": 84.1, "branches": 70.2, "functions": 80.5, "report": "coverage/backend/lcov.info" }
281
+ }
282
+ }
283
+ ]
284
+ }
285
+ ```
286
+
287
+ 字段说明:
288
+
289
+ - **`suites[]`** 按模块 / 测试文件分组,承载**两个正交维度**:`layer`(`frontend` / `backend` / `e2e`,QA 主视图的三条泳道)与 `level`(`unit` / `integration` / `e2e`)。`files` 只用于定位,可省。
290
+ - **`cases[]`** 一条用例 = 一次能判定成败的验证:
291
+ - `automated` 区分自动化 / 手工;**手工用例也要登记**,`steps` 一步一条,这是交接给 QA 的正文;
292
+ - `covers` 是覆盖的验收,写法 `S-001-A1`(故事 id + 第几条验收,**1 基**);改验收顺序要同步改它,一条 E2E 用例可以覆盖多个故事;
293
+ - `expect` 必填(只有标题的用例不算交付物);`taskId` 指向产出它的 TDD 任务(可省);`code` 是自动化用例的定位串 `文件::用例名`,导入器靠它回写状态;
294
+ - `status` 只认 `pass / fail / skipped / blocked / not-run`;`not-run` 不要求 `lastRun`,其余必须写。
295
+ - **`runs[]`** 每次执行一条,**按时间正序**(最后一条 = 最近一次):`at` 到秒、`command`、`env`、`totals`、`failedCases`,有覆盖率报告就填 `coverage`。
296
+ - `coverage` 按层给 `lines / branches / functions / statements` 与 `tool`:**前端、后端各报各的,绝不做加权平均**,E2E 一般不报行覆盖。
297
+
298
+ 覆盖率的三个口径(页面按这个算,禁止合成一个「覆盖率」):
299
+
300
+ | 口径 | 分母 | 分子 |
301
+ |---|---|---|
302
+ | 需求覆盖 | 非 `draft` / `cancelled` 故事的验收条数 | 被至少一条用例 `covers` 命中的条数;其中最近一次 `pass` 的另算「验证通过」 |
303
+ | 用例通过率 | 最近一次执行的 `total - skipped` | `pass` |
304
+ | 代码覆盖 | 测试工具报告 | `lines` / `branches`,前端、后端分开 |
305
+
306
+ 校验器强制:枚举合法;用例 / 套件 id 与其它记录共用一个命名空间;`covers` 指向的验收必须存在且序号不越界;`taskId` 必须存在且属于 `covers` 里的故事;`runs` 时间正序;`通过+失败+跳过 ≤ 总数`;最新一次 `failedCases` 必须对应 `status: fail`;覆盖率在 0~100 之间。
307
+
308
+ 收口纪律(**只查登记过用例的故事**,避免老项目一升级就满屏报错):
309
+
310
+ - 该 story 下每个 `done` 的 `type: test` task 至少一条用例 → 否则**报错**;
311
+ - story 到 `review` / `done` 时每条验收都被覆盖 → 否则**报错**(未到 review 只给 WARN);
312
+ - 用例 `lastRun` 早于 task 最后一次 `pass` → WARN(结果过期了);
313
+ - 最近一次执行条数多于已登记用例 → WARN(还有测试没登记)。
314
+
237
315
  ## 状态机(校验器强制)
238
316
 
239
317
  - story: `draft → ready → in-progress → review → done`;不做了直接进 `cancelled`(已取消)终态(可回退,不可跳级;`done` 与 `cancelled` 都是终态)
@@ -243,7 +321,7 @@ node tools/last-change.mjs write <记录.json> --replace # 确认旧记录不
243
321
 
244
322
  ## 硬约束(校验器强制 / 警告)
245
323
 
246
- 1. ID 全局唯一(epic / story / task / sprint / milestone 共用命名空间),task 命名 `<storyId>-T<n>`
324
+ 1. ID 全局唯一(epic / story / task / sprint / milestone / suite / case 共用命名空间),task 命名 `<storyId>-T<n>`
247
325
  2. **双向引用一致**:`epic.stories` ↔ `story.epicId`、`sprint.stories` ↔ `story.sprintId`、`journal.taskId` ↔ task
248
326
  3. `ready` / `in-progress` / `review` / `done` 必须有验收标准;进 sprint 的故事必须有验收标准(`cancelled` 两样都不要求)
249
327
  4. 验收标准的 `verifyBy` 必须指向**同一 story 的** task
@@ -18,7 +18,7 @@ npx scrum-skills serve # 默认当前目录 http://localhost:4173
18
18
  npx scrum-skills serve . 8080 # 指定目录与端口
19
19
  ```
20
20
 
21
- server 读取 `agile/` 三文件(backlog / sprints / journal),每次请求现取,不写文件。改了数据刷新页面即生效。
21
+ server 读取 `agile/` 下的数据文件(backlog / sprints / journal,加可选的 tests / last-change),每次请求现取,不写文件。改了数据刷新页面即生效。
22
22
 
23
23
  ## 页面
24
24
 
@@ -29,6 +29,7 @@ server 读取 `agile/` 三文件(backlog / sprints / journal),每次请求
29
29
  - `/gantt` 甘特图:按迭代分带;浅色条是 `story.schedule` 计划区间,实心条是 journal 推出的真实执行区间;菱形是里程碑(MVP 琥珀色),红色虚线是今天;行内标出依赖数量与阻塞,图下列出依赖链与阻塞清单
30
30
  - `/milestones` 里程碑 / MVP 节点:按 kind 分组(mvp / release / checkpoint),显示达成口径、挂的故事、进度与剩余天数
31
31
  - `/burndown` 燃尽图:可选任意迭代;实线来自每个 task 最后一条 pass 的日期,虚线是理想线,逐日格子给出实际/理想剩余
32
+ - `/tests` 测试用例:QA 交付视图——需求覆盖分子/分母(按验收条)、前端/后端/E2E 分层概览与故事×层矩阵、用例清单(可展开步骤与预期)、执行记录、缺口与风险(未覆盖验收 + 最近失败用例);数据来自 `agile/tests.json`,没有该文件时给空状态
32
33
  - `/story/S-001` 追溯链:acceptance → task → journal 每阶段(red/green/refactor)的完整记录,含测试命令与产出文件
33
34
 
34
35
  ## 纪律
@@ -37,3 +38,5 @@ server 读取 `agile/` 三文件(backlog / sprints / journal),每次请求
37
38
  2. 报表只读 JSON,不许为展示改数据;发现数据缺漏时向用户报告并用 planning/sprint/tdd skill 补
38
39
  3. 向用户汇报进度时引用具体 id(S-001、S-001-T2),不许说「差不多了」「快完成了」这类没有数据支撑的口径
39
40
  4. 燃尽图数据来自 journal 的 date 字段;某 task 没写 journal 导致曲线缺点的,如实显示缺口并指出责任 story
41
+ 5. 报覆盖率必须说清是哪一本账:需求覆盖(验收条)、用例通过(最近一次执行)、代码覆盖(各层工具报告,前端/后端分开),不许把三者合成一个「覆盖率」数
42
+ 6. `/tests` 的缺口就是交付风险:汇报进度时一并说「N 条验收没用例」「M 条用例最近失败」,别只报通过率
@@ -30,7 +30,7 @@ description: 管理 Sprint 迭代周期与 Story 状态流转,落库到 agile/
30
30
  - story: `draft → ready → in-progress → review → done`
31
31
  - task: `todo → red → green → refactored → done`
32
32
  - 每次流转必须同步落库并运行 `node tools/validate.mjs`
33
- - story 到 `review` 的条件:所有 task 为 `done` 且验收标准逐条核对过
33
+ - story 到 `review` 的条件:所有 task 为 `done`、验收标准逐条核对过,且**每条验收至少被一条用例覆盖**(`agile/tests.json`;缺口在 `/tests` 页「缺口与风险」里,没用的补用例,手工验的登记成 `automated: false`)
34
34
  - story 到 `done` 由用户确认,AI 不许自行关闭
35
35
 
36
36
  ### 收尾 / 回顾
@@ -41,14 +41,26 @@ description: 以 red-green-refactor TDD 循环驱动任务实施,并把每一
41
41
 
42
42
  1. 最后一次全量测试通过,journal 末条 `result` 为 `pass`
43
43
  2. **不许凭断言关单**:「测试过了应该没问题」不算验证。有可运行入口的(页面/命令/接口)要真正跑起来按 acceptance 逐条核一遍;跑不了的也要向用户说明「验证到了哪一步、哪一步没法验」
44
- 3. task 状态 → `done`,`files` 补齐本次触碰的文件
45
- 4. story 的所有 task 都 done → 提醒用户进入 review(scrum-sprint skill)
44
+ 3. 把本次新增 / 修改的用例与这一次的执行结果写进 `agile/tests.json`(见下节第 1 步),再推状态
45
+ 4. task 状态 → `done`,`files` 补齐本次触碰的文件
46
+ 5. story 的所有 task 都 done → 提醒用户进入 review(scrum-sprint skill)
46
47
 
47
48
  ## 交工汇报(每做完一项功能必做)
48
49
 
49
- task 推到 `done` 之后、向用户回话之前,两件事缺一不可。
50
+ task 推到 `done` 之后、向用户回话之前,三件事缺一不可。
50
51
 
51
- ### 1. 写 `agile/last-change.json`:先看 git,再决定覆盖还是合并
52
+ ### 1. 更新 `agile/tests.json`:本次用例 + 这一次的执行结果
53
+
54
+ **什么时候写**:每个 task 到 `done` 都要写(不是攒到迭代末);story 送 `review` 前补齐该 story 的缺口。
55
+
56
+ - 用例登记在 `suites[]` 下:suite 定 `layer`(`frontend` / `backend` / `e2e`)与 `level`(`unit` / `integration` / `e2e`),用例写 `title` / `expect`,`covers` 指向验收(`S-001-A1`,序号是 1 基);
57
+ - 手工验证也要登记成 `automated: false` 的用例,`steps` 一步一条——QA 交接靠它;
58
+ - 每次跑完测试往 `runs[]` 追加一条:`at`(到时分秒)、`command`、`env`、`totals`(total / pass / fail / skipped)、`failedCases`,有覆盖率报告就填 `coverage`(前端 / 后端分开;E2E 一般不报行覆盖);
59
+ - 再把对应用例的 `status` 与 `lastRun` 刷成这次的结果(`not-run` 表示还没跑过);
60
+ - 写完跑 `node tools/validate.mjs`:枚举、`covers` 引用、执行记录与用例状态一致性都会被查,报错口径见 schema 第 5 节;
61
+ - **没真跑过不许写 `pass`**;失败了就写 `fail`,原因写进用例的 `note`。
62
+
63
+ ### 2. 写 `agile/last-change.json`:先看 git,再决定覆盖还是合并
52
64
 
53
65
  **写之前必须先判断这个文件提交了没有**——上一次交付很可能还没 commit,直接覆盖就把它冲掉了:
54
66
 
@@ -83,7 +95,7 @@ node tools/last-change.mjs write <本次记录.json> # 已提交 → 覆盖
83
95
 
84
96
  写完整份跑 `node tools/validate.mjs`:校验器会查 id 是否真实存在、枚举是否合法、场景有没有验收标准、调试地址是不是完整 http(s) 地址。
85
97
 
86
- ### 2. 按同一顺序回话给用户
98
+ ### 3. 按同一顺序回话给用户
87
99
 
88
100
  变更表(故事/任务 · 新增还是状态变更 · 改了什么 · 涉及文件)→ 额外做的事 → 验证结果 → 账号密码与访问地址 → 测试前准备 → 逐条测试场景(步骤 + 验收标准)→ 说明。**缺哪项就不提那节,但不许只说「做完了」**,也不许用「应该没问题」代替验证记录。最后告诉用户:起 `node tools/serve.mjs` 后到 `/last-change` 页能看到同一份内容。
89
101
 
@@ -109,5 +121,7 @@ node tools/last-change.mjs write <本次记录.json> # 已提交 → 覆盖
109
121
  - 不许为让测试通过而删除/跳过/弱化测试;确需改测试时在 journal 的 note 里说明理由
110
122
  - 一次只做一个 task 的一个阶段,禁止批量推进多个 task 状态
111
123
  - 交工前没做真实验证不许把 task 置 `done`(跑不起入口的须在 journal 里写明验证边界)
124
+ - 没更新 `agile/tests.json` 不算交付完成:用例与执行结果和代码一样是交付物,`/tests` 页读的就是它
125
+ - 用例状态不许编:`pass` 必须是真跑出来的,跟 `runs` 里的失败清单对不上会被校验器打回
112
126
  - 没写 `agile/last-change.json` 不算交付完成:交付说明和代码一样是交付物
113
127
  - 写它之前没查 git 状态(该合并却整份覆盖)同样算违规:会把上一次还没提交的记录冲掉
package/README.md CHANGED
@@ -10,7 +10,7 @@ AI 只负责把需求、任务、实施过程写成**结构化 JSON**,人打
10
10
 
11
11
  - **AI 写代码很快,但「做到哪了」全靠人问** → 每个故事/任务的状态、实施日志都落在 `agile/` 里,随时可查
12
12
  - **TDD 靠自觉,容易悄悄跳过** → 校验器强制「先有失败的测试,才有实现」:task 过了 red 阶段,日志里必须有一条 `fail` 记录和一条 `pass` 记录
13
- - **看板工具要么太重、要么数据锁死** → 三个 JSON 文件 + 一个零依赖 server,数据在你自己仓库里,`git diff` 就能审
13
+ - **看板工具要么太重、要么数据锁死** → 几个 JSON 文件 + 一个零依赖 server,数据在你自己仓库里,`git diff` 就能审
14
14
 
15
15
  ## 快速开始
16
16
 
@@ -28,14 +28,14 @@ npx scrum-skills validate
28
28
  npx scrum-skills serve # → http://localhost:4173
29
29
  ```
30
30
 
31
- `init` 会建出 `agile/` 的三个文件骨架,并把四个 skill 装进 `.agents/skills/`:
31
+ `init` 会建出 `agile/` 的数据骨架(含空的 `tests.json`),并把四个 skill 装进 `.agents/skills/`:
32
32
 
33
33
  | skill | 干什么 |
34
34
  |---|---|
35
35
  | `scrum-planning` | 需求澄清 → 写史诗/故事/验收标准/拆任务 |
36
36
  | `scrum-sprint` | 排期、组建迭代、状态流转、里程碑、收尾 |
37
37
  | `scrum-tdd` | red → green → refactor 循环 + 过程落库 |
38
- | `scrum-report` | 看板 / 甘特 / 里程碑 / 燃尽 / 追溯链 |
38
+ | `scrum-report` | 看板 / 甘特 / 里程碑 / 燃尽 / 测试用例 / 追溯链 |
39
39
 
40
40
  ## 命令
41
41
 
@@ -43,7 +43,7 @@ npx scrum-skills serve # → http://localhost:4173
43
43
  |---|---|
44
44
  | `scrum-skills init [dir] [--force]` | 初始化(已有 `AGENTS.md` 时拒绝,`--force` 先备份成 `AGENTS.md.<时间戳>.bak` 再覆盖) |
45
45
  | `scrum-skills update [dir] [--dry-run]` | 只刷新框架文件(skill / AGENTS.md / 校验器),**绝不碰 `agile/` 数据**;除 `AGENTS.md` 外一律无条件覆盖、不备份,`AGENTS.md` 覆盖前备份成 `AGENTS.md.<时间戳>.bak` |
46
- | `scrum-skills validate [dir]` | 校验数据(引用完整性 / 状态机 / TDD 纪律 / 里程碑 / 依赖无环) |
46
+ | `scrum-skills validate [dir]` | 校验数据(引用完整性 / 状态机 / TDD 纪律 / 测试资产 / 里程碑 / 依赖无环) |
47
47
  | `scrum-skills serve [dir] [port]` | 起看板(默认当前目录,端口 4173),**校验不通过时拒绝出报表**;打开的就是启动目录,上次打开过的项目只留在「最近项目」里供手动切回 |
48
48
 
49
49
  `scsk` 是 `scrum-skills` 的等价简写,全局安装后两个命令都可直接用,如 `scsk serve`。
@@ -57,13 +57,14 @@ npx scrum-skills serve # → http://localhost:4173
57
57
 
58
58
  ## 数据长什么样
59
59
 
60
- `agile/` 下只有三个文件:
60
+ `agile/` 下三个常驻文件 + 两个可选文件:
61
61
 
62
62
  | 文件 | 内容 |
63
63
  |---|---|
64
64
  | `agile/backlog.json` | 史诗 `epics`(含量化目标 `keyResults`)+ 故事 `stories`(内联 `tasks`) |
65
65
  | `agile/sprints.json` | 迭代 `sprints` + 里程碑 `milestones`(MVP 节点 = `kind: "mvp"`) |
66
66
  | `agile/journal.json` | 每个任务一条 TDD 日志 `journals`(内联 `entries`) |
67
+ | `agile/tests.json` | 测试资产(可选):`suites` 按层 × 级别装用例(`covers` 指向验收),`runs` 存执行记录与代码覆盖率;结构见 schema 第 5 节 |
67
68
  | `agile/last-change.json` | 最近一次交付的汇报(可选;已提交才覆盖,未提交则合并):变更清单 / 额外工作 / 验证 / 入口与账号 / 测试前准备 / 测试场景 / 说明 |
68
69
 
69
70
  ```jsonc
@@ -101,11 +102,12 @@ npx scrum-skills serve # → http://localhost:4173
101
102
  | 页面 | 内容 |
102
103
  |---|---|
103
104
  | `/` 看板 | 六列(草稿/就绪/进行中/评审/完成/已取消),按史诗/里程碑筛选,按优先级或 WSJF 重排;卡片带类型、阻塞、范围变更标记 |
104
- | `/last-change` 最后更新 | 渲染 `agile/last-change.json`:故事/任务变更清单、额外做的事、验证结果、调试入口与账号、测试前准备、逐条测试场景、说明与遗留;点「查看历史」按 git 提交翻看任一版本(只读,顶部页签可在最新/历史间切换);没有该文件时给空状态 |
105
+ | `/last-change` 最后更新 | 渲染 `agile/last-change.json`:故事/任务变更清单、额外做的事、验证结果、调试入口与账号、测试前准备、逐条测试场景、说明与遗留;变更清单抬头可「复制变更信息」(生成 Conventional Commits 提交信息)或「自动提交」(看清信息与文件清单后 `git add -A` + `git commit`,只提交到本地、不 push);点「查看历史」按 git 提交翻看任一版本(只读,顶部页签可在最新/历史间切换);没有该文件时给空状态 |
105
106
  | `/epics` 史诗与目标 | 每个史诗的关键结果(KR)进度、故事完成度、阻塞与缺陷统计 |
106
107
  | `/gantt` 甘特图 | 迭代分带,浅色条=计划、实心条=journal 推出的实际执行区间,菱形=里程碑,依赖与阻塞清单 |
107
108
  | `/milestones` 里程碑 / MVP | 按 kind 分组,显示达成口径、挂的故事、剩余天数 |
108
109
  | `/burndown` 燃尽图 | 实线=真实剩余,虚线=理想线,竖线标出**哪一天插了需求**(范围变更) |
110
+ | `/tests` 测试用例 | QA 交付视图:需求覆盖(按验收条的分子/分母)、前端/后端/E2E 分层概览与**故事 × 层覆盖矩阵**、用例清单(可展开步骤与预期)、执行记录、缺口与风险(未覆盖验收 + 最近失败用例);没有 `agile/tests.json` 时给空状态并说明谁来写 |
109
111
  | `/story/:id` 追溯链 | 验收标准 → 任务 → 每条 red/green/refactor 记录,含测试命令与产出文件 |
110
112
 
111
113
  > **并排指标卡的配色顺序固定为 绿 → 蓝 → 紫 → 黑,卡片更多才往后用黄、红**。顺序只在 `apps/board/app/composables/model.ts` 的 `CARD_TONES` 里定义一次,页面写 `:class="cardToneClass(i)"`、光斑写 `:tone="cardTone(i)"`;黑卡是深底,文字要翻白。新增页面照着取色,别另起一套。
@@ -173,6 +175,8 @@ node tools/last-change.mjs write 记录.json --replace # 确认旧记录不要
173
175
  - 依赖:`dependsOn` 必须存在、不能自依赖、**不能成环**(DFS 检测)
174
176
  - 需求属性:`type` / `priority` / `value` 合法;`type: bug` 必须有 `severity` + `environment` + `steps`
175
177
  - 验收标准的 `verifyBy` 必须指向同一 story 的 task
178
+ - 测试资产(`agile/tests.json`,可选):`layer`/`level`/`status` 枚举合法、用例 id 唯一、`covers` 指向的验收必须存在且序号不越界、`taskId` 属于 covers 里的故事、`runs` 时间正序、最新一次 `failedCases` 必须对应 `status: fail`、覆盖率 0~100
179
+ - **收口纪律**(只查登记过用例的故事):done 的 `test` 型任务必须有对应用例;story 到 `review`/`done` 时每条验收必须被覆盖
176
180
  - 迭代 id 格式与序号连续、同时只能有一个活跃迭代
177
181
  - 里程碑 `reached` 时挂的故事必须全部完成
178
182
 
@@ -186,7 +190,7 @@ AGILE_DIR=/别的项目 pnpm dev # 看别的项目的数据
186
190
 
187
191
  pnpm build # 构建看板静态产物 → assets/board
188
192
  pnpm validate # 只校验数据
189
- pnpm test # 回归测试(校验器 / 看板皮肤与配色 / 布局回归 / 卡片操作 / 项目目录 / 最后更新页与历史版本 / 交付说明写入策略 / AI 服务与面板,159 个用例)
193
+ pnpm test # 回归测试(校验器 / 测试资产与覆盖计算 / 看板皮肤与配色 / 布局回归 / 卡片操作 / 项目目录 / 最后更新页与历史版本 / 交付说明写入策略 / 提交信息与自动提交 / AI 服务与面板,204 个用例)
190
194
  pnpm serve # 起看板(读目标目录的 agile/,同时提供 /api/ai)
191
195
  ```
192
196
 
@@ -1 +1 @@
1
- <!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>敏捷看板</title><script type="importmap">{"imports":{"#entry":"/_nuxt/CkF353aX.js"}}</script><link rel="stylesheet" href="/_nuxt/entry.Dx3CNA--.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CkF353aX.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DK3Fl9T5.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D2O8Q8x9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DZo3dYLJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/51OWHj8c.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B3jkABw9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/6gFtTBwm.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DSaWoFHS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Csqgy925.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C7K0R7F-.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/ChG76uh9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CJ2mz0WY.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DEfLc7p3.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DLkAKotw.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Vo4wMbnS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CuJUHLl8.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CK9_veoz.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bb3Limos.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Dtsa-1NS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Ci0qbkgh.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BuOKDFiJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D3YCAmfu.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B_SlFcWp.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bi9Axj3V.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/rlePnzpc.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DfR3IZN_.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/EYTShbt4.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BgLeKwx0.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C9StZL3D.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BPTrRhcw.js"><script type="module" src="/_nuxt/CkF353aX.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/_nuxt/CSSb154I.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/DHihWhf5.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-404.BFmXOnCT.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/B5n2_jZN.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-500.BZ-DdjQ6.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/ZVTqot-_.js"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="apple-touch-icon" href="/favicon.svg"></head><body><div id="__nuxt" class="isolate"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/",buildId:"d7f0931f-32b3-4c23-b05f-7cbf5851b226",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793369477277,false]</script></body></html>
1
+ <!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>敏捷看板</title><script type="importmap">{"imports":{"#entry":"/_nuxt/DkI1Qxpp.js"}}</script><link rel="stylesheet" href="/_nuxt/entry.CKHePurH.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DkI1Qxpp.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DK3Fl9T5.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D2O8Q8x9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DZo3dYLJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/51OWHj8c.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B3jkABw9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/6gFtTBwm.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DSaWoFHS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Csqgy925.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C7K0R7F-.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/ChG76uh9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CJ2mz0WY.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DEfLc7p3.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DLkAKotw.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Vo4wMbnS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CuJUHLl8.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CK9_veoz.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bb3Limos.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Dtsa-1NS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Ci0qbkgh.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BuOKDFiJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D3YCAmfu.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B_SlFcWp.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bi9Axj3V.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/rlePnzpc.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DfR3IZN_.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/EYTShbt4.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BgLeKwx0.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C9StZL3D.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BPTrRhcw.js"><script type="module" src="/_nuxt/DkI1Qxpp.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/_nuxt/CSSb154I.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/DHihWhf5.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-404.BFmXOnCT.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/B5n2_jZN.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-500.BZ-DdjQ6.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/ZVTqot-_.js"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="apple-touch-icon" href="/favicon.svg"></head><body><div id="__nuxt" class="isolate"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/",buildId:"8e23aec9-5b67-4569-b8bf-d4f07e50ffdf",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793377640997,false]</script></body></html>
@@ -1 +1 @@
1
- <!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>敏捷看板</title><script type="importmap">{"imports":{"#entry":"/_nuxt/CkF353aX.js"}}</script><link rel="stylesheet" href="/_nuxt/entry.Dx3CNA--.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CkF353aX.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DK3Fl9T5.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D2O8Q8x9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DZo3dYLJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/51OWHj8c.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B3jkABw9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/6gFtTBwm.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DSaWoFHS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Csqgy925.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C7K0R7F-.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/ChG76uh9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CJ2mz0WY.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DEfLc7p3.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DLkAKotw.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Vo4wMbnS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CuJUHLl8.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CK9_veoz.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bb3Limos.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Dtsa-1NS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Ci0qbkgh.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BuOKDFiJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D3YCAmfu.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B_SlFcWp.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bi9Axj3V.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/rlePnzpc.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DfR3IZN_.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/EYTShbt4.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BgLeKwx0.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C9StZL3D.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BPTrRhcw.js"><script type="module" src="/_nuxt/CkF353aX.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/_nuxt/CSSb154I.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/DHihWhf5.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-404.BFmXOnCT.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/B5n2_jZN.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-500.BZ-DdjQ6.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/ZVTqot-_.js"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="apple-touch-icon" href="/favicon.svg"></head><body><div id="__nuxt" class="isolate"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/",buildId:"d7f0931f-32b3-4c23-b05f-7cbf5851b226",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793369477277,false]</script></body></html>
1
+ <!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>敏捷看板</title><script type="importmap">{"imports":{"#entry":"/_nuxt/DkI1Qxpp.js"}}</script><link rel="stylesheet" href="/_nuxt/entry.CKHePurH.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DkI1Qxpp.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DK3Fl9T5.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D2O8Q8x9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DZo3dYLJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/51OWHj8c.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B3jkABw9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/6gFtTBwm.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DSaWoFHS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Csqgy925.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C7K0R7F-.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/ChG76uh9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CJ2mz0WY.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DEfLc7p3.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DLkAKotw.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Vo4wMbnS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CuJUHLl8.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CK9_veoz.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bb3Limos.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Dtsa-1NS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Ci0qbkgh.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BuOKDFiJ.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/D3YCAmfu.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/B_SlFcWp.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Bi9Axj3V.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/rlePnzpc.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DfR3IZN_.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/EYTShbt4.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BgLeKwx0.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/C9StZL3D.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BPTrRhcw.js"><script type="module" src="/_nuxt/DkI1Qxpp.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/_nuxt/CSSb154I.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/DHihWhf5.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-404.BFmXOnCT.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/B5n2_jZN.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-500.BZ-DdjQ6.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/ZVTqot-_.js"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="apple-touch-icon" href="/favicon.svg"></head><body><div id="__nuxt" class="isolate"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/",buildId:"8e23aec9-5b67-4569-b8bf-d4f07e50ffdf",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793377640997,false]</script></body></html>
@@ -0,0 +1 @@
1
+ import{D as e,E as t,On as n,Yt as r,Zn as i,_ as a,b as o,bt as s,er as c,g as l,k as u,mt as d,o as f,qt as p,tr as m,un as ee,v as te,vn as h,y as g}from"./DZo3dYLJ.js";import{w as ne,x as re}from"./51OWHj8c.js";import{t as ie}from"./CJ2mz0WY.js";import{t as ae}from"./rlePnzpc.js";import{a as _,i as v,o as y,r as oe,s as b}from"#entry";var se={class:`tests-page`},ce={class:`page-anchor`,"aria-label":`页内跳转`},le=[`href`,`onClick`],ue={id:`tc-overview`,class:`kpis`},de={class:`v`},fe={class:`v`},pe={class:`v`},me={class:`v`},he={class:`v`},ge={key:0,class:`callout soft`},_e={class:`layers`},ve={class:`layer-hd`},ye={class:`tag tag-neutral`},be={class:`layer-body`},xe={class:`ok`},Se={class:`bad`},Ce={class:`dim`},x={class:`dim`},S={id:`tc-coverage`,class:`card panel`},C={class:`card-bd cover-grid`},w={class:`cover-main`},T={class:`cover-big`},E={class:`sub`},D={class:`bar`},O={class:`cover-sub`},k={class:`cover-side`},A={key:0,class:`foot dim`},we={id:`tc-matrix`,class:`card`},Te={class:`card-hd`},Ee={class:`tag tag-neutral`},De={class:`filters inline`},Oe=[`onClick`],ke={class:`n`},Ae={class:`card-bd matrix-wrap`},je={class:`tbl`},Me={class:`ttl`},Ne={class:`dim`},Pe={class:`foot dim`},Fe={key:0},Ie={id:`tc-cases`,class:`card`},Le={class:`card-hd`},Re={class:`tag tag-neutral`},ze={class:`card-bd`},Be={class:`filters`},Ve=[`onClick`],He={class:`n`},Ue=[`onClick`],We={class:`n`},Ge={class:`tbl cases`},Ke=[`onClick`],qe={class:`mono`},Je={key:0,class:`code mono`},Ye={class:`tag tag-neutral`},Xe={class:`dim lv`},Ze={class:`mono covers`},Qe={key:0,class:`dim`},$e={class:`dim`},et={class:`r mono dim`},tt={colspan:`6`},nt={class:`detail`},rt={key:0,class:`steps`},it={key:1,class:`dim`},at={class:`dim`},ot={key:0},st={key:1},ct={key:2},lt={key:0,class:`foot dim`},ut={key:1,class:`foot dim`},dt={id:`tc-gaps`,class:`card`},ft={class:`card-hd`},pt={class:`card-bd gap-grid`},mt={class:`h3`},ht={key:0,class:`gap-list`},gt={class:`dim`},_t={class:`gap-text`},vt={key:1,class:`foot dim`},yt={class:`h3`},bt={key:0,class:`gap-list`},xt={class:`mono`},St={key:0,class:`dim`},Ct={key:1,class:`foot dim`},wt={id:`tc-runs`,class:`card`},Tt={class:`card-hd`},Et={class:`tag tag-neutral`},Dt={class:`card-bd`},Ot={class:`tbl`},kt={class:`mono`},At={class:`mono`},jt={class:`dim`},Mt={class:`r mono`},Nt={class:`r mono ok`},Pt={class:`r mono dim`},Ft={class:`r mono dim`},It={class:`mono dim cov-cell`},Lt={key:0},Rt={key:0,class:`foot dim`},zt={key:1,class:`foot dim`},j=v(u({__name:`tests`,setup(u){let{testCases:v,testStats:j,requirement:M,matrixRows:N,lastRun:P,testRuns:F,coverageRows:I,LAYERS:L,LAYER_LABEL:R,LEVEL_LABEL:Bt,CASE_STATUS_LABEL:z,CASE_STATUS_COLOR:Vt,CASE_STATUS_DOT:Ht,storyById:Ut,STATE_LABEL:Wt,PRIORITY_COLOR:B,md:Gt}=b(),V=h(`all`),H=h(`all`),U=h(``),W=h(``),Kt=[{id:`tc-overview`,label:`概览`},{id:`tc-coverage`,label:`覆盖率`},{id:`tc-matrix`,label:`覆盖矩阵`},{id:`tc-cases`,label:`用例清单`},{id:`tc-gaps`,label:`缺口与风险`},{id:`tc-runs`,label:`执行记录`}],qt=e=>{let t=document.getElementById(e);t&&(t.scrollIntoView({behavior:`smooth`,block:`start`}),history.replaceState(null,``,`#${e}`))},Jt=[`#8a63f7`,`#3f7fd4`,`#6aa844`],G=l(()=>[{key:`all`,label:`全部`,count:j.value.total},...L.map(e=>({key:e,label:R[e],count:j.value.byLayer[e]?.total??0}))]),K=l(()=>{let e=U.value.trim().toLowerCase();return v.value.filter(t=>V.value!==`all`&&t.layer!==V.value||H.value!==`all`&&t.status!==H.value?!1:!e||[t.id,t.title,t.code,t.suiteName,...t.covers??[]].join(` `).toLowerCase().includes(e))}),Yt=l(()=>{let e=P.value?.totals;if(e){let t=e.total-(e.skipped??0);return t>0?Math.round(e.pass/t*100):0}let t=j.value.total-j.value.byStatus[`not-run`];return t>0?Math.round((j.value.byStatus.pass??0)/t*100):0}),q=h(`covered`),Xt=l(()=>N.value.filter(e=>q.value===`all`?!0:q.value===`covered`?e.covered>0:e.covered===0)),J=l(()=>N.value.filter(e=>e.covered===0).length),Zt=l(()=>[{key:`covered`,label:`有用例`,count:N.value.filter(e=>e.covered>0).length},{key:`missing`,label:`未覆盖`,count:J.value},{key:`all`,label:`全部`,count:N.value.length}]),Y=e=>I.value.find(t=>t.layer===e),X=l(()=>v.value.filter(e=>e.status===`fail`)),Z=l(()=>[`pass`,`fail`,`skipped`,`blocked`,`not-run`].map(e=>({key:e,label:z[e],count:j.value.byStatus[e]??0}))),Q=e=>typeof e==`number`?e>=1e3?`${(e/1e3).toFixed(1)}s`:`${e}ms`:`—`,Qt=e=>{W.value=W.value===e?``:e},$=e=>[...new Set((e.covers??[]).map(e=>e.replace(/-A\d+$/,``)))].map(e=>Ut.value.get(e)).filter(Boolean);return(l,u)=>{let h=oe,b=ae,I=ie;return d(),o(`div`,se,[a(`nav`,ce,[u[2]||=a(`span`,{class:`anchor-label`},`跳到`,-1),(d(),o(f,null,s(Kt,e=>a(`a`,{key:e.id,href:`#${e.id}`,class:`anchor-chip`,onClick:ne(t=>qt(e.id),[`prevent`])},m(e.label),9,le)),64))]),a(`div`,ue,[a(`div`,{class:i([`kpi`,(`cardToneClass`in l?l.cardToneClass:n(y))(0)])},[e(h,{tone:(`cardTone`in l?l.cardTone:n(_))(0),size:`sm`},null,8,[`tone`]),u[3]||=a(`p`,{class:`k`},`用例总数`,-1),a(`p`,de,[t(m(n(j).total),1),a(`small`,null,`自动 `+m(n(j).automated)+` / 手工 `+m(n(j).manual),1)])],2),a(`div`,{class:i([`kpi`,(`cardToneClass`in l?l.cardToneClass:n(y))(1)])},[e(h,{tone:(`cardTone`in l?l.cardTone:n(_))(1),size:`sm`},null,8,[`tone`]),u[5]||=a(`p`,{class:`k`},`用例通过率`,-1),a(`p`,fe,[t(m(n(Yt)),1),u[4]||=a(`small`,null,`%`,-1)])],2),a(`div`,{class:i([`kpi`,(`cardToneClass`in l?l.cardToneClass:n(y))(2)])},[e(h,{tone:(`cardTone`in l?l.cardTone:n(_))(2),size:`sm`},null,8,[`tone`]),u[6]||=a(`p`,{class:`k`},`需求覆盖`,-1),a(`p`,pe,[t(m(n(M).covered),1),a(`small`,null,`/ `+m(n(M).total)+` 条验收`,1)])],2),a(`div`,{class:i([`kpi`,(`cardToneClass`in l?l.cardToneClass:n(y))(3)])},[e(h,{tone:(`cardTone`in l?l.cardTone:n(_))(3),size:`sm`},null,8,[`tone`]),u[7]||=a(`p`,{class:`k`},`前端行覆盖率`,-1),a(`p`,me,[t(m(Y(`frontend`)?.lines??`—`),1),a(`small`,null,m(Y(`frontend`)?`%`:`未上报`),1)])],2),a(`div`,{class:i([`kpi`,(`cardToneClass`in l?l.cardToneClass:n(y))(4)])},[e(h,{tone:(`cardTone`in l?l.cardTone:n(_))(4),size:`sm`},null,8,[`tone`]),u[8]||=a(`p`,{class:`k`},`后端行覆盖率`,-1),a(`p`,he,[t(m(Y(`backend`)?.lines??`—`),1),a(`small`,null,m(Y(`backend`)?`%`:`未上报`),1)])],2)]),n(v).length?(d(),o(f,{key:1},[a(`div`,_e,[(d(!0),o(f,null,s(n(L),(e,t)=>(d(),o(`div`,{key:e,class:`card layer-card`},[a(`div`,ve,[a(`span`,{class:`layer-dot`,style:c({background:Jt[t]})},null,4),a(`b`,null,m(n(R)[e]),1),a(`span`,ye,m(n(j).byLayer[e]?.total??0),1)]),a(`div`,be,[a(`span`,xe,`通过 `+m(n(j).byLayer[e]?.pass??0),1),a(`span`,Se,`失败 `+m(n(j).byLayer[e]?.fail??0),1),a(`span`,Ce,`跳过 `+m(n(j).byLayer[e]?.skipped??0),1),a(`span`,x,`未跑 `+m(n(j).byLayer[e]?.notRun??0),1)])]))),128))]),a(`div`,S,[u[12]||=a(`div`,{class:`card-hd`},[a(`h2`,{class:`h2`},`覆盖率`),a(`span`,{class:`tag tag-neutral`},`需求口径`),a(`span`,{class:`dim tip`},`分母是非草稿 / 未取消故事的验收条数`)],-1),a(`div`,C,[a(`div`,w,[a(`p`,T,[t(m(n(M).covered),1),a(`span`,null,`/`+m(n(M).total),1)]),a(`p`,E,`验收已被用例覆盖 `+m(n(M).rate)+`%`,1),a(`div`,D,[a(`i`,{style:c({width:n(M).rate+`%`})},null,4)]),a(`p`,O,[u[10]||=t(` 其中最近一次通过验证 `,-1),a(`b`,null,m(n(M).verified),1),t(` / `+m(n(M).total)+`(`+m(n(M).verifiedRate)+`%) `,1)]),u[11]||=a(`p`,{class:`foot dim`},`有用例但最近一次失败的不算「验证通过」——覆盖与通过是两件事。`,-1)]),a(`div`,k,[(d(!0),o(f,null,s(n(Z),e=>(d(),o(`div`,{key:e.key,class:`stat-row`},[a(`i`,{class:`dot`,style:c({background:n(Ht)[e.key]})},null,4),a(`span`,null,m(e.label),1),a(`b`,null,m(e.count),1)]))),128)),n(P)?(d(),o(`p`,A,[t(` 最近一次:`+m(n(P).at)+` · `+m(n(P).command||`未记录命令`),1),n(P).env?(d(),o(f,{key:0},[t(` · `+m(n(P).env),1)],64)):g(``,!0)])):g(``,!0)])])]),a(`div`,we,[a(`div`,Te,[u[13]||=a(`h2`,{class:`h2`},`分层覆盖矩阵`,-1),a(`span`,Ee,m(n(N).length),1),u[14]||=a(`span`,{class:`dim tip`},`前端 / 后端 / E2E 三条泳道分开看,— 表示这一层没有用例`,-1),a(`div`,De,[(d(!0),o(f,null,s(n(Zt),e=>(d(),o(`button`,{key:e.key,class:i([`chip`,{on:n(q)===e.key}]),onClick:t=>q.value=e.key},[t(m(e.label),1),a(`span`,ke,m(e.count),1)],10,Oe))),128))])]),a(`div`,Ae,[a(`table`,je,[u[15]||=a(`thead`,null,[a(`tr`,null,[a(`th`,null,`故事`),a(`th`,null,`优先级`),a(`th`,{class:`r`},`前端`),a(`th`,{class:`r`},`后端`),a(`th`,{class:`r`},`E2E`),a(`th`,{class:`r`},`验收覆盖`),a(`th`,null,`状态`)])],-1),a(`tbody`,null,[(d(!0),o(f,null,s(n(Xt),r=>(d(),o(`tr`,{key:r.id},[a(`td`,null,[e(I,{to:`/story/${r.id}`,class:`mono link`},{default:p(()=>[t(m(r.id),1)]),_:2},1032,[`to`]),a(`span`,Me,m(r.title),1)]),a(`td`,null,[a(`span`,{class:i([`tag`,`tag-${n(B)[r.priority]??`neutral`}`])},m(r.priority),3)]),a(`td`,{class:i([`r mono`,{zero:!r.layers.frontend}])},m(r.layers.frontend||`—`),3),a(`td`,{class:i([`r mono`,{zero:!r.layers.backend}])},m(r.layers.backend||`—`),3),a(`td`,{class:i([`r mono`,{zero:!r.layers.e2e}])},m(r.layers.e2e||`—`),3),a(`td`,{class:i([`r mono`,{bad:!r.allCovered}])},m(r.covered)+`/`+m(r.total),3),a(`td`,null,[a(`span`,Ne,m(n(Wt)[r.status]??r.status),1)])]))),128))])]),a(`p`,Pe,[u[16]||=t(` 一格 = 这一层覆盖了该故事几条验收;三个 — 全空就是完全没测。 `,-1),n(J)?(d(),o(`span`,Fe,`还有 `+m(n(J))+` 个故事一条用例都没有,切「未覆盖」查看。`,1)):g(``,!0)])])]),a(`div`,Ie,[a(`div`,Le,[u[17]||=a(`h2`,{class:`h2`},`用例清单`,-1),a(`span`,Re,m(n(K).length),1),r(a(`input`,{"onUpdate:modelValue":u[0]||=e=>ee(U)?U.value=e:null,class:`input case-search`,placeholder:`搜编号 / 标题 / 文件 / 验收`,"aria-label":`搜索用例`},null,512),[[re,n(U)]])]),a(`div`,ze,[a(`div`,Be,[(d(!0),o(f,null,s(n(G),e=>(d(),o(`button`,{key:e.key,class:i([`chip`,{on:n(V)===e.key}]),onClick:t=>V.value=e.key},[t(m(e.label),1),a(`span`,He,m(e.count),1)],10,Ve))),128)),u[18]||=a(`span`,{class:`sep`},null,-1),a(`button`,{class:i([`chip`,{on:n(H)===`all`}]),onClick:u[1]||=e=>H.value=`all`},`全部状态`,2),(d(!0),o(f,null,s(n(Z),e=>(d(),o(`button`,{key:e.key,class:i([`chip`,{on:n(H)===e.key}]),onClick:t=>H.value=e.key},[t(m(e.label),1),a(`span`,We,m(e.count),1)],10,Ue))),128))]),a(`table`,Ge,[u[23]||=a(`thead`,null,[a(`tr`,null,[a(`th`,null,`编号`),a(`th`,null,`用例`),a(`th`,null,`层 / 级别`),a(`th`,null,`覆盖验收`),a(`th`,null,`方式`),a(`th`,null,`状态`),a(`th`,{class:`r`},`最近执行`)])],-1),a(`tbody`,null,[(d(!0),o(f,null,s(n(K),e=>(d(),o(f,{key:e.id},[a(`tr`,{class:`case-row`,onClick:t=>Qt(e.id)},[a(`td`,qe,m(e.id),1),a(`td`,null,[a(`b`,null,m(e.title),1),e.code?(d(),o(`span`,Je,m(e.code),1)):g(``,!0)]),a(`td`,null,[a(`span`,Ye,m(n(R)[e.layer]??e.layer),1),a(`span`,Xe,m(n(Bt)[e.level]??e.level),1)]),a(`td`,Ze,[(d(!0),o(f,null,s(e.covers??[],e=>(d(),o(`span`,{key:e,class:`cov-ref`},m(e),1))),128)),(e.covers??[]).length?g(``,!0):(d(),o(`span`,Qe,`—`))]),a(`td`,null,[a(`span`,$e,m(e.automated?`自动`:`手工`),1)]),a(`td`,null,[a(`span`,{class:i([`tag`,`tag-${n(Vt)[e.status]??`neutral`}`])},m(n(z)[e.status]??e.status),3)]),a(`td`,et,m(e.lastRun?n(Gt)(e.lastRun):`—`),1)],8,Ke),n(W)===e.id?(d(),o(`tr`,{key:e.id+`-d`,class:`detail-row`},[u[22]||=a(`td`,null,null,-1),a(`td`,tt,[a(`div`,nt,[a(`p`,null,[u[19]||=a(`b`,null,`预期:`,-1),t(m(e.expect),1)]),(e.steps??[]).length?(d(),o(`ol`,rt,[(d(!0),o(f,null,s(e.steps,(e,t)=>(d(),o(`li`,{key:t},m(e),1))),128))])):g(``,!0),e.note?(d(),o(`p`,it,[u[20]||=a(`b`,null,`备注:`,-1),t(m(e.note),1)])):g(``,!0),a(`p`,at,[u[21]||=t(` 覆盖故事: `,-1),(d(!0),o(f,null,s($(e),e=>(d(),te(I,{key:e.id,to:`/story/${e.id}`,class:`mono link`},{default:p(()=>[t(m(e.id),1)]),_:2},1032,[`to`]))),128)),$(e).length?(d(),o(`span`,ot,` · 套件 `+m(e.suiteName)+`(`+m(e.suiteId)+`)`,1)):(d(),o(`span`,st,`未关联故事 · 套件 `+m(e.suiteName)+`(`+m(e.suiteId)+`)`,1)),e.durationMs===void 0?g(``,!0):(d(),o(`span`,ct,` · 耗时 `+m(Q(e.durationMs)),1))])])])])):g(``,!0)],64))),128))])]),n(K).length?(d(),o(`p`,ut,`点一行展开预期与步骤;手工用例按「步骤 + 预期」交接给 QA。`)):(d(),o(`p`,lt,`当前筛选下没有用例。`))])]),a(`div`,dt,[a(`div`,ft,[u[24]||=a(`h2`,{class:`h2`},`缺口与风险`,-1),a(`span`,{class:i([`tag`,n(M).gaps.length||n(X).length?`tag-error`:`tag-success`])},m(n(M).gaps.length+n(X).length),3),u[25]||=a(`span`,{class:`dim tip`},`未覆盖的验收 + 最近一次失败的用例`,-1)]),a(`div`,pt,[a(`div`,null,[a(`p`,mt,[e(b,{name:`i-lucide-shield-alert`,class:`size-3`}),t(`没有任何用例覆盖的验收(`+m(n(M).gaps.length)+`)`,1)]),n(M).gaps.length?(d(),o(`ul`,ht,[(d(!0),o(f,null,s(n(M).gaps,r=>(d(),o(`li`,{key:r.ref},[a(`span`,{class:i([`tag`,`tag-${n(B)[r.priority]??`neutral`}`])},m(r.priority),3),e(I,{to:`/story/${r.storyId}`,class:`mono link`},{default:p(()=>[t(m(r.ref),1)]),_:2},1032,[`to`]),a(`span`,gt,m(r.storyTitle),1),a(`span`,_t,m(r.text),1)]))),128))])):(d(),o(`p`,vt,`每条验收都有对应用例。`))]),a(`div`,null,[a(`p`,yt,[e(b,{name:`i-lucide-circle-x`,class:`size-3`}),t(`最近一次失败的用例(`+m(n(X).length)+`)`,1)]),n(X).length?(d(),o(`ul`,bt,[(d(!0),o(f,null,s(n(X),e=>(d(),o(`li`,{key:e.id},[a(`span`,xt,m(e.id),1),a(`b`,null,m(e.title),1),e.note?(d(),o(`span`,St,`· `+m(e.note),1)):g(``,!0)]))),128))])):(d(),o(`p`,Ct,`没有失败用例。`))])])]),a(`div`,wt,[a(`div`,Tt,[u[26]||=a(`h2`,{class:`h2`},`执行记录`,-1),a(`span`,Et,m(n(F).length),1),u[27]||=a(`span`,{class:`dim tip`},`最近 20 次,最新一条在最后`,-1)]),a(`div`,Dt,[a(`table`,Ot,[u[28]||=a(`thead`,null,[a(`tr`,null,[a(`th`,null,`时间`),a(`th`,null,`命令`),a(`th`,null,`环境`),a(`th`,{class:`r`},`总数`),a(`th`,{class:`r`},`通过`),a(`th`,{class:`r`},`失败`),a(`th`,{class:`r`},`跳过`),a(`th`,{class:`r`},`耗时`),a(`th`,null,`代码覆盖`)])],-1),a(`tbody`,null,[(d(!0),o(f,null,s(n(F),(e,t)=>(d(),o(`tr`,{key:e.id??e.at+t,class:i({last:t===n(F).length-1})},[a(`td`,kt,m(e.at),1),a(`td`,At,m(e.command||`—`),1),a(`td`,jt,m(e.env||`—`),1),a(`td`,Mt,m(e.totals?.total??`—`),1),a(`td`,Nt,m(e.totals?.pass??`—`),1),a(`td`,{class:i([`r mono`,{bad:e.totals?.fail}])},m(e.totals?.fail??`—`),3),a(`td`,Pt,m(e.totals?.skipped??`—`),1),a(`td`,Ft,m(Q(e.totals?.durationMs)),1),a(`td`,It,[(d(!0),o(f,null,s(e.coverage??{},(e,t)=>(d(),o(`span`,{key:t},m(n(R)[t]??t)+` `+m(e.lines??`—`)+`%`,1))),128)),Object.keys(e.coverage??{}).length?g(``,!0):(d(),o(`span`,Lt,`—`))])],2))),128))])]),n(F).length?(d(),o(`p`,zt,`覆盖率来自各层测试工具的报告,前端 / 后端分开,不做加权平均。`)):(d(),o(`p`,Rt,`还没有执行记录:跑完测试后把结果追加到 runs,页面上才有通过率与趋势。`))])])],64)):(d(),o(`div`,ge,[e(b,{name:`i-lucide-flask-conical`,class:`size-4`}),u[9]||=a(`div`,null,[a(`b`,null,`还没有测试用例`),a(`p`,{style:{"margin-top":`3px`}},[t(` 项目里没有 `),a(`code`,{class:`mono`},`agile/tests.json`),t(`,或者它还没有登记用例。 按纪律,Coding Agent 每推完一个 task / story 就该把本次新增用例与执行结果写进来: 结构见 `),a(`code`,{class:`mono`},`scrum-planning`),t(` 的 `),a(`code`,{class:`mono`},`references/schema.md`),t(` 第 5 节。 `)])],-1)]))])}}}),[[`__scopeId`,`data-v-d69ddc53`]]);export{j as default};