@rowansenne/workflow-run 0.19.0 → 0.20.0
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/CHANGELOG.md +11 -1
- package/README.md +2 -2
- package/create-release-md-doc/SKILL.md.tpl +2 -2
- package/create-tech-md-doc/SKILL.md.tpl +2 -2
- package/dev/SKILL.md.tpl +2 -2
- package/package.json +1 -1
- package/project-review/SKILL.md.tpl +24 -10
- package/project-review/references/checklist.md.tpl +1 -0
- package/project-review/references/diff-strategy.md +32 -1
- package/project-review/references/report.md.tpl +5 -3
- package/workflow/references/requirements.md +3 -2
- package/workflow/scripts/artifacts.mjs +2 -2
- package/workflow/scripts/requirement.mjs +50 -18
- package/workflow/scripts/view.mjs +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.20.0] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### 变更
|
|
10
|
+
- **project-review 独立复核**:能派子 agent 的环境(Claude Code、Codex、Qwen Code 等)里,CRITICAL / HIGH 每条交给一个只拿到结论、位置、触发条件的子 agent 重新读代码判定「成立 / 可能成立 / 不成立」,不成立的删掉并在「未覆盖与待确认」记复核排除的条数;派不了时自己复核,先找不成立的理由。复核方式写进报告「审查方式」。
|
|
11
|
+
- **project-review 分角度并行查找**(深审大改动,用户确认后):深审、改动超过 30 个文件或 1500 行、环境能派子 agent 时,先问用户要不要分 5 个角度(正确性与字段完整性 / 并发事务与幂等 / 安全与异常 / 数据库与配置 / 性能测试与语言专项)各派一个子 agent 同时查;同意后主 agent 合并去重、做影响面,再交独立复核。不同意照旧分批精读。
|
|
12
|
+
- **project-review 审查深度与降噪**:分快审 / 标准(默认)/ 深审,写进「审查方式」。快审只查正确性、空值、安全、事务与幂等;标准不查风格;深审全部维度并复核 MEDIUM。新增「只报会出问题的」:风格、linter 能拦的、个人偏好、写不出后果的建议不报,LOW 只写有实际收益的优化。
|
|
13
|
+
- **需求目录里每种产物只留最新版,文件名固定**(`review.md`、`tech-doc.md` …,不再生成 `_v2`、`_v3`),上线方案引用技术方案、复审引用的产物等不会因为出了新版而失效。版本号记在 `meta.json` 的 `versions`。`requirement.mjs path` 发现已有旧版时,先把它复制到 `workflow-run/tmp/old-artifacts/<需求>/<种类>_v<N>.md`(`previous` 指向它,复审对照用),新版直接写到原文件;`sync <path>` 时内容变了才算新版本,输出 `archived` 列出归档的文件,由 skill 提示用户确认后自己删除。`/workflow report`、`/workflow view` 只统计目录里的最新版,`view` 显示版本号。
|
|
14
|
+
- `/workflow view` 的审查记录按报告内容判断首次 / 复审(原来按排序位置,只剩一份报告时会把复审标成首次)。
|
|
15
|
+
|
|
7
16
|
## [0.19.0] - 2026-10-18
|
|
8
17
|
|
|
9
18
|
### 变更(破坏性)
|
|
@@ -251,7 +260,8 @@
|
|
|
251
260
|
- 安装器改为单一 Node 实现,`init-skills.sh` / `init-skills.ps1` 只是薄封装。
|
|
252
261
|
- `code-review` 改名为 `project-review`,升级时自动清理旧名。
|
|
253
262
|
|
|
254
|
-
[Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.
|
|
263
|
+
[Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.20.0...HEAD
|
|
264
|
+
[0.20.0]: https://github.com/guilianerde/skills-init/compare/v0.19.0...v0.20.0
|
|
255
265
|
[0.19.0]: https://github.com/guilianerde/skills-init/compare/v0.18.0...v0.19.0
|
|
256
266
|
[0.18.0]: https://github.com/guilianerde/skills-init/compare/v0.17.0...v0.18.0
|
|
257
267
|
[0.17.0]: https://github.com/guilianerde/skills-init/compare/v0.16.0...v0.17.0
|
package/README.md
CHANGED
|
@@ -477,13 +477,13 @@ workflow-run/docs/requirements/<需求名>/
|
|
|
477
477
|
├── meta.json # 需求名、绑定的分支、各产物(版本、日期、wiki 发布记录、DocNexus 沉淀),给以后的可视化用
|
|
478
478
|
├── plan.md # 开发方案(dev,L 规模)
|
|
479
479
|
├── delivery.md # 交付说明(dev,每次完整交付都写)
|
|
480
|
-
├── review.md # 审查报告(project-review
|
|
480
|
+
├── review.md # 审查报告(project-review);复审也写这个文件,上一版归档到 workflow-run/tmp/old-artifacts/
|
|
481
481
|
├── tech-doc.md # 技术方案
|
|
482
482
|
└── release-doc.md # 上线方案
|
|
483
483
|
```
|
|
484
484
|
|
|
485
485
|
* **需求名由你定**:dev 确定是 M / L 规模(要落盘)时,或从 `/workflow` 完整链路开始时,如果当前分支还没有绑定需求,会问你这个需求叫什么(默认值按分支名生成;也可以选已有的需求,例如修 bug 的新分支挂到原需求下)。你不起名就按分支名生成。绑定后,同一分支上的各 skill 都写到这个目录,不再问。一个需求可以绑定多个分支。
|
|
486
|
-
* 路径由 `workflow/scripts/requirement.mjs path <种类>`
|
|
486
|
+
* 路径由 `workflow/scripts/requirement.mjs path <种类>` 给出,每种产物文件名固定,目录里只留最新版,引用不会失效;版本号记在 `meta.json` 的 `versions`。已有旧版时,上一版先复制到 `workflow-run/tmp/old-artifacts/<需求>/<种类>_v<N>.md`(不提交),新版直接覆盖写入;skill 会列出归档的文件,请你确认后自己删除。上一版发布过 wiki 时,新版沿用同一个页面。
|
|
487
487
|
* 需求目录写在项目根的 `.ignore` 里,ripgrep 和 agent 的代码搜索会跳过它,产物多了不会污染搜索结果(找历史决策用 DocNexus)。git 不受影响,建议随代码提交。
|
|
488
488
|
|
|
489
489
|
**独立文档仓库(可选)**:不想让产物进代码仓库、或一个需求横跨多个服务时,装好 skills 后在项目根运行一条命令:
|
|
@@ -69,10 +69,10 @@ description: "Generate a Markdown release/deployment plan (上线方案) from th
|
|
|
69
69
|
|
|
70
70
|
### Step 4 — 写文件
|
|
71
71
|
|
|
72
|
-
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path release --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/release-doc.md
|
|
72
|
+
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path release --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/release-doc.md`,文件名固定;已有旧版时直接写新版,上一版由脚本归档到输出的 `previous`)。规则见 `<skills>/workflow/references/requirements.md`。
|
|
73
73
|
- 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" --branch <dev分支>`;用户不给名字时 `bind` 按分支名生成。
|
|
74
74
|
- `wiki_record` 非空:上一版已发布到 wiki,新版第一行原样写这条记录,重新发布时更新同一页面。
|
|
75
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
75
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
76
76
|
- 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <文档路径> --skill create-release-md-doc --facts` 自检(`--facts` 按元信息行的对比分支核对正文里的类名、文件、接口、表名):有「错误」先修正再交付(缺必填章节、残留 `{{...}}` 占位符、缺元信息行);「警告」按实际情况处理,其中「在 diff 和代码里都找不到」的名字逐个核实:确属编造就改成 diff 里的真实名字或写「待补充」,是外部系统 / 配置中心的名字就保留;「提示」里的待填写位置在交付摘要中告诉用户。
|
|
77
77
|
- 再把路径和摘要告诉用户。
|
|
78
78
|
|
|
@@ -67,10 +67,10 @@ description: "Generate a Markdown technical design doc (技术方案) from the d
|
|
|
67
67
|
|
|
68
68
|
### Step 4 — 写文件
|
|
69
69
|
|
|
70
|
-
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path tech --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/tech-doc.md
|
|
70
|
+
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path tech --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/tech-doc.md`,文件名固定;已有旧版时直接写新版,上一版由脚本归档到输出的 `previous`)。规则见 `<skills>/workflow/references/requirements.md`。
|
|
71
71
|
- 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" --branch <dev分支>`;用户不给名字时 `bind` 按分支名生成。
|
|
72
72
|
- `wiki_record` 非空:上一版已发布到 wiki,新版第一行原样写这条记录,重新发布时更新同一页面。
|
|
73
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
73
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
74
74
|
- 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <文档路径> --skill create-tech-md-doc --facts` 自检(`--facts` 按元信息行的对比分支核对正文里的类名、文件、接口、表名):有「错误」先修正再交付(缺必填章节、残留 `{{...}}` 占位符、缺元信息行);「警告」按实际情况处理,其中「在 diff 和代码里都找不到」的名字逐个核实:确属编造就改成 diff 里的真实名字或写「待补充」,是外部系统 / 配置中心的名字就保留;「提示」里的待填写位置在交付摘要中告诉用户。
|
|
75
75
|
- 再把路径和摘要告诉用户。
|
|
76
76
|
|
package/dev/SKILL.md.tpl
CHANGED
|
@@ -155,8 +155,8 @@ description: "Implement a feature or fix a bug in this project end to end: under
|
|
|
155
155
|
产物按需求归档在 `workflow-run/docs/requirements/<需求名>/`,规则见 `<skills>/workflow/references/requirements.md`:
|
|
156
156
|
|
|
157
157
|
- **需求名**:确定规模为 M / L(要落盘)时,先运行 `node <skills>/workflow/scripts/requirement.mjs which`;当前分支还没绑定需求就问用户这个需求叫什么(默认值用输出的 `suggested_name`),再 `requirement.mjs bind --name "<需求名>"`;用户不给名字时 `bind` 按分支名生成。S 规模快速通道不落盘,不问。缺陷修复同样处理(可以选择挂到原需求下)。
|
|
158
|
-
- **路径**:交付说明用 `requirement.mjs path delivery`,L 规模方案(或用户要求落盘的 M 规模方案)用 `requirement.mjs path plan`,写到输出的 `path
|
|
159
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
158
|
+
- **路径**:交付说明用 `requirement.mjs path delivery`,L 规模方案(或用户要求落盘的 M 规模方案)用 `requirement.mjs path plan`,写到输出的 `path`;文件名固定,已有旧版时直接写新版,上一版由脚本归档(输出的 `previous`)。
|
|
159
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
160
160
|
|
|
161
161
|
## 终端命令
|
|
162
162
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-review
|
|
3
|
-
description: "Review source code (any language; Java/Spring, Go, TypeScript, Python, etc.) for bugs, correctness, missing fields, null-safety, concurrency, exception handling, resource leaks
|
|
3
|
+
description: "Review source code (any language; Java/Spring, Go, TypeScript, Python, etc.) for bugs, correctness, missing fields, null-safety, concurrency, exception handling, resource leaks and security issues (style only when asked). Use when the user asks to review code, review a branch/PR/MR against another branch (e.g. compare `main`), check a class/method/entity for missing fields or bugs, compare two classes to see whether a converter/mapper (DTO ↔ VO ↔ Entity, BeanUtils/MapStruct) drops or mismaps fields, verify changes before merge, or wants a structured code audit with an actionable findings report. Trigger phrases: 帮我审代码 / 审这个分支 / 这个类这段代码有没有问题 / 还缺哪些字段 / 是不是漏字段了 / 对照一下两个类 / 转换方法漏没漏字段 / review this branch / compare with main / code review / 影响面分析. Reports findings only and does not change code unless asked. Not for implementing or fixing (dev), reviewing the wording of a document, or generating tech/release docs."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Code Review
|
|
7
7
|
|
|
8
8
|
## Workflow
|
|
9
9
|
|
|
10
|
-
1.
|
|
10
|
+
1. **确定审查范围与深度**:范围是单文件 / 单方法、分支对比(如 `branch` vs `main`),还是未提交改动(`git diff HEAD`,见 `references/diff-strategy.md`「未提交改动」)。深度按用户的说法定,没说就用「标准」,写进报告「审查方式」:
|
|
11
|
+
- **快审**(「快速看一下」「扫一眼」):只查正确性、空值、安全、事务与幂等,不查性能、测试、风格;「优化建议(LOW)」写「无」。CRITICAL / HIGH 照样复核。
|
|
12
|
+
- **标准**(默认):检查清单全部维度,**风格类(清单「一致性 / 风格」一节)不查**;「优化建议(LOW)」只写有实际收益的(性能、可维护性上的具体问题),没有就写「无」。
|
|
13
|
+
- **深审**(「仔细审」「全面审」,或资金 / 权限 / 对外接口的大改动合并前):全部维度,包括风格;MEDIUM 也复核。
|
|
11
14
|
2. **建立理解**:先读实体、接口、DTO、调用链,理清数据流再下结论;不要凭文件名猜内容。
|
|
12
15
|
- **可选:查项目约定(DocNexus)**:DocNexus 可用时(`resolve.mjs status` 的 `docnexus.ok`),先运行 `node <skills>/workflow/scripts/knowledge.mjs project-review --match <模块名 / 业务名词> --query "<一句话:改动涉及的模块与业务>"`,看以往审查沉淀的反复问题和开发约定(`--query` 在这些分类内按正文 recall;输出 `recall_by_category: false` 时改按 `docnexus-recall` 做不带分类的 recall),找相关的编码约定、历史决策与已知坑,作为审查依据并在报告中引用 `docnexus/library/<path>`。未检测到或查询失败就跳过,并在「未覆盖与待确认」中注明;审查过程中**不写入** DocNexus(写入见第 8 步)。
|
|
13
16
|
- **同分支已有产物**:运行 `node <skills>/workflow/scripts/artifacts.mjs --branch <dev分支>`(单文件 / 未提交改动用当前分支,省略 `--branch`),看输出的 `latest`:
|
|
@@ -15,20 +18,31 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
15
18
|
- `latest.delivery` 存在(dev 的交付说明)→ 其中「已确认的决定」并入需求对照的依据;「配置清单」「SQL 脚本」和 diff 对照,diff 里有而交付说明漏列的配置 key / DDL 记为 MEDIUM(上线时会被漏掉)。
|
|
16
19
|
- `latest.review` 存在 → 这是**复审**:读上次报告的问题表,并用 `git log --oneline --since=<上次报告日期> <dev分支>` 看之后改了什么。本次报告对上次的每条问题标「已修复 / 仍存在 / 不再适用」(「已修复」要看到对应改动),本次新发现的标「新增」。
|
|
17
20
|
- **涉及数据库**:diff 里有 SQL、Mapper、实体或 DDL 时,按 `<skills>/workflow/references/tables.md` 运行 `node <skills>/workflow/scripts/tables.mjs --diff <基线>...<dev>`(未提交改动用 `--base <基线>`)找出涉及的表;数据库已配置(`resolve.mjs status` 的 `database.ok`)时用 `query.mjs --schema --tables <表> --indexes` 只读拉取实际字段、索引与估算行数,作为第 4、6 步判断索引支撑、字段存在与类型、DDL 锁表与滚动发布兼容的依据。查不了库时注明「未与实际库核对」写进「未覆盖与待确认」,这类发现不升到 HIGH 以上,除非 diff 本身就能证明。
|
|
18
|
-
3. **分支对比**:基线默认取 `workflow/profile.json` 的 `main_branch`(未配置则 `main`)。按 `references/diff-strategy.md` 先量规模:小改动全量精读;改动多时按风险分级(P0 资金 / 事务 / 权限 / SQL / 对外接口 / MQ /
|
|
21
|
+
3. **分支对比**:基线默认取 `workflow/profile.json` 的 `main_branch`(未配置则 `main`)。按 `references/diff-strategy.md` 先量规模:小改动全量精读;改动多时按风险分级(P0 资金 / 事务 / 权限 / SQL / 对外接口 / MQ / 配置优先)分批精读,审不到的写进报告。**深审且改动属于「更大」、环境又能派子 agent 时,先问用户要不要分角度并行查找**(`references/diff-strategy.md`「分角度并行查找」),用户同意才走,第 4 步改由各子 agent 按角度过清单。只审本次改动引入的问题,同时检查改动是否破坏既有调用方。
|
|
19
22
|
4. **逐项过检查清单**(在项目根运行 `node <skills>/workflow/scripts/resolve.mjs template project-review checklist.md`,读输出的 `path`;退出码 2 时停下并按 `hint` 处理):对每个发现给出 文件+行号、问题描述、严重级别、修改建议。
|
|
20
23
|
5. **影响面分析**:改动是否影响其他调用方/模块。先运行 `node <skills>/code-graph/scripts/cg.mjs impact --diff <基线>...<dev>`(code-graph skill;未提交改动用 `--base <基线>`):列出受影响的接口、MQ 消费者、定时任务、测试、表和配置 key,报告「影响面」按入口归纳并带文件:行号;退出码 2(没有索引等)时用 grep 或精读调用链确认。Spring 注入、AOP、MyBatis XML、MQ topic、配置 key 图谱看不到,照常 grep(见 `<skills>/workflow/references/codegraph.md`)。查不到调用方的符号不能当作安全,要明说"未确认"。
|
|
21
|
-
6. **复核**:交付前逐条复核 CRITICAL / HIGH
|
|
24
|
+
6. **复核**:交付前逐条复核 CRITICAL / HIGH(深审时 MEDIUM 也复核;其余 MEDIUM 至少做下面第 1 条)。审查结论会被用来决定能不能合并,一条误报会让人白改代码,也会让后面的真问题不被当回事。找出问题的人自己复核容易顺着原来的思路认同自己,所以**能交给独立的一方就交出去**:
|
|
25
|
+
- **独立复核(首选)**:当前环境能派子 agent / 子任务时(例如 Claude Code 的 Agent 工具、Codex / Qwen Code 的子 agent),每条发现派一个,互不相关的可以同时派。只给它:一句话结论、位置(文件:行号)、你写的触发条件、对比分支(或「未提交改动」)。**不给你的推理过程**,也不告诉它你认为成立。要它自己读代码,按下面 1~4 条检查,回答「成立 / 可能成立 / 不成立」并引用代码作为依据。
|
|
26
|
+
- **自行复核**:环境派不了子 agent 时自己做,先找它**不成立**的理由,找不到再确认成立。
|
|
27
|
+
检查项(独立复核和自行复核都按这 4 条):
|
|
22
28
|
1. 回到被审的代码(分支对比用 `git show <dev>:<文件>`,未提交改动读工作区)确认引用的片段和行号真实存在。
|
|
23
29
|
2. 写出**触发条件**:什么输入 / 状态下会出错、错成什么样。写不出来 → 降为 MEDIUM 并标「⚠ 待确认」。
|
|
24
30
|
3. 找有没有别处已经兜住:调用方校验、`@Validated` / 全局异常处理、数据库约束、框架默认行为、配置。涉及索引、字段、唯一约束、表大小的发现,以第 2 步拉到的实际表结构为准(复核列写「已与 <连接名> 核对」)。已兜住 → 删掉或降级,并在说明里写依据。
|
|
25
31
|
4. 确认是本次改动引入的:改动前就存在的问题移到「未覆盖与待确认」,不计入合并结论。
|
|
26
|
-
|
|
32
|
+
结果:「成立」在报告「复核」列标「✅」;「可能成立」标「⚠」;「不成立」从问题表里删掉(独立复核否掉的,在「未覆盖与待确认」里记一句「复核排除 N 条」)。复核方式(独立 / 自行)写进报告「审查方式」元信息。
|
|
27
33
|
7. **输出报告**:按报告模板输出(`node <skills>/workflow/scripts/resolve.mjs template project-review report.md` 取 `path`;退出码 2 时停下并按 `hint` 处理),落盘规则见 Artifact Location。
|
|
28
34
|
8. **沉淀到 DocNexus(需用户授权)**:`resolve.mjs status` 的 `docnexus.ok` 为 true 时,交付报告后**每次都问**用户「要不要启动 DocNexus 流程,把本次审查沉淀到 DocNexus?」,并从本次发现里列出建议沉淀的内容:同类问题再次出现(知识库里已有类似沉淀,或本次多处出现)、新确立的编码约定、容易踩的坑;没有特别值得记的也照问,并说明这一点。未接入时不提问,报告的「DocNexus」元信息行写「未接入」。
|
|
29
35
|
- 用户同意 → 按 DocNexus 入口 skill `docnexus` 的 capture 流程写入;正文只写知识,不写分支、路径;有 facts 时按 `<skills>/workflow/references/docnexus.md`「沉淀的来源」的格式写 provenance;分类用 `node <skills>/workflow/scripts/resolve.mjs category project-review` 的结果作为调用方显式分类封存(见 `<skills>/workflow/references/docnexus.md`「写入 DocNexus 的分类」)。只沉淀问题模式和约定,不沉淀某次审查的逐条清单,也不沉淀「⚠ 待确认」的发现;知识库已有同类沉淀时更新那一篇(带 `--base-version`),不要重复新建。写入成功后把报告的「DocNexus」元信息行改成 `docnexus/library/<path>`(多篇用「、」分隔),并回报给用户。
|
|
30
36
|
- 用户不同意 → 元信息行保持「未沉淀」。
|
|
31
37
|
|
|
38
|
+
## 只报会出问题的
|
|
39
|
+
|
|
40
|
+
报告越短,真问题越不会被漏看。下面这些不报:
|
|
41
|
+
- 风格、命名、格式、注释措辞(快审和标准审查都不报;深审或用户要求时才看);
|
|
42
|
+
- 编译器、linter、CI 已经会拦下的(未使用的变量、格式、import 顺序);
|
|
43
|
+
- 个人偏好、没有违反项目约定(`workflow/profile.json` 的 `notes`、DocNexus 约定、审查清单)的写法差异;
|
|
44
|
+
- 写不出后果的「可以更好」:每条发现都要说清不改会怎样。
|
|
45
|
+
|
|
32
46
|
## Review Dimensions
|
|
33
47
|
|
|
34
48
|
- 正确性 / 逻辑 bug
|
|
@@ -38,7 +52,7 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
38
52
|
- 异常处理与资源释放(连接、流、锁、事务)
|
|
39
53
|
- 安全(注入 / XSS / 越权 / 敏感信息 / 硬编码密钥)
|
|
40
54
|
- 性能(N+1、循环内查询、大对象、无谓复制)
|
|
41
|
-
- 一致性 / 命名 /
|
|
55
|
+
- 一致性 / 命名 / 风格(只在深审或用户要求时)
|
|
42
56
|
- 幂等与消息一致性(重复消费 / 回调、事务内发消息)
|
|
43
57
|
- 数据库变更安全(锁表、滚动发布兼容、索引支撑;以实际表结构为准,见第 2 步「涉及数据库」)
|
|
44
58
|
- 测试(改动有没有被测试覆盖、断言是否有效)
|
|
@@ -50,7 +64,7 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
50
64
|
- **CRITICAL**:崩溃 / 数据错误 / 资金影响 / 安全漏洞,必须改
|
|
51
65
|
- **HIGH**:明显 bug,特定场景可触发
|
|
52
66
|
- **MEDIUM**:潜在问题,边界未处理或语义存疑
|
|
53
|
-
- **LOW
|
|
67
|
+
- **LOW**:有实际收益的优化建议(性能、可维护性);风格类只在深审时报
|
|
54
68
|
|
|
55
69
|
## Output Template
|
|
56
70
|
|
|
@@ -62,10 +76,10 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
62
76
|
|
|
63
77
|
审查报告按需求归档在 `workflow-run/docs/requirements/<需求名>/`,规则见 `<skills>/workflow/references/requirements.md`:
|
|
64
78
|
|
|
65
|
-
- 路径用 `node <skills>/workflow/scripts/requirement.mjs path review`(分支对比按 dev 分支加 `--branch <dev分支>`),写到输出的 `path
|
|
79
|
+
- 路径用 `node <skills>/workflow/scripts/requirement.mjs path review`(分支对比按 dev 分支加 `--branch <dev分支>`),写到输出的 `path`(文件名固定为 `review.md`);已有报告时脚本先把它归档,输出的 `previous` 是上次报告,复审对照它。
|
|
66
80
|
- 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" [--branch <dev分支>]`;用户不给名字时 `bind` 按分支名生成。
|
|
67
81
|
- 报告同时完整输出到对话;落盘文件与对话内容一致
|
|
68
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`
|
|
82
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`
|
|
69
83
|
- 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <报告路径> --skill project-review --facts` 自检:「错误」(缺必填章节、缺元信息行、残留 `{{...}}`)先修正;「在 diff 和代码里都找不到」的名字逐个核实,写错了就改成真实的名字
|
|
70
84
|
|
|
71
85
|
## Verification
|
|
@@ -73,7 +87,7 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
73
87
|
- 每个结论都要能定位到具体文件+行号,并引用真实代码片段。
|
|
74
88
|
- 读不到的内容如实说明,不要臆测。
|
|
75
89
|
- 涉及跨分支对比时,确保 diff 基于正确的 base 分支(默认 `main`)。
|
|
76
|
-
- CRITICAL / HIGH 都经过第 6
|
|
90
|
+
- CRITICAL / HIGH 都经过第 6 步复核(能派子 agent 时独立复核),写得出触发条件;复核不了的标「⚠ 待确认」,不计入「不建议合并」的结论。
|
|
77
91
|
- 分批审查时,没精读的文件列在「未覆盖与待确认」里,不能让读者以为全部审过。
|
|
78
92
|
|
|
79
93
|
## 终端命令
|
|
@@ -39,6 +39,37 @@ git diff <main>...<dev> --name-only -G '@Transactional|synchronized|Lock\b|@Vers
|
|
|
39
39
|
- 每批结束先把发现记下来(文件:行号 + 一句话)再进入下一批,防止后面的批次挤掉前面的结论;批次多时可以边审边把发现写进报告草稿。
|
|
40
40
|
- 时间或上下文不够时,**P2 可以只看 `--stat`**,但要在报告「未覆盖与待确认」里列出只扫过 `--stat` 的文件;P0 不能跳过。
|
|
41
41
|
|
|
42
|
-
## 4.
|
|
42
|
+
## 4. 分角度并行查找(深审大改动,用户确认后)
|
|
43
|
+
|
|
44
|
+
一个人拿着整份清单从头扫到尾,清单越长、diff 越大,后面的维度越容易被草草带过。改动大时可以让几个子 agent 各盯一类问题,同时看同一份 diff。它比分批精读慢、用量多几倍,所以**三个条件都满足才问用户**,用户同意才走:
|
|
45
|
+
|
|
46
|
+
1. 审查深度是**深审**(见 SKILL.md 第 1 步);
|
|
47
|
+
2. 规模属于上表的「更大」(排除后 > 30 个文件或 > 1500 行改动);
|
|
48
|
+
3. 当前环境能派子 agent / 子任务(Claude Code 的 Agent 工具、Codex / Qwen Code 的子 agent 等)。
|
|
49
|
+
|
|
50
|
+
问法:「改动较大(<N> 个文件 / <M> 行),要不要分 5 个角度派子 agent 并行查找?更全面,但耗时和用量是平时的几倍。」用户不同意、或没回答就继续,按上一节分批精读。
|
|
51
|
+
|
|
52
|
+
**5 个角度**(对应检查清单的小节):
|
|
53
|
+
|
|
54
|
+
| 角度 | 清单小节 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| 正确性与字段完整性 | 1 正确性、2 空值、3 字段完整性 |
|
|
57
|
+
| 并发、事务与幂等 | 4 并发与线程安全、11 幂等与消息一致性 |
|
|
58
|
+
| 安全与异常 | 5 异常处理与资源释放、6 安全 |
|
|
59
|
+
| 数据库与配置 | 10 配置、12 数据库变更安全 |
|
|
60
|
+
| 性能、测试与语言专项 | 7 性能、13 测试、14 语言专项(深审时 8 风格也放这里) |
|
|
61
|
+
|
|
62
|
+
改动里没有对应内容的角度不派(例如没有 SQL / Mapper / DDL / 配置改动时不派「数据库与配置」)。
|
|
63
|
+
|
|
64
|
+
**每个子 agent 只给**:审查范围与对比分支(读 diff 的命令)、自己那几节清单的原文、第 2 步收集的背景(需求对照要点、实际表结构、DocNexus 约定、`config_center`),以及「只报本次改动引入的问题,按 P0 → P1 → P2 读,每条给 文件:行号、一句话结论、触发条件、级别;读不完的文件列出来」。不给其他角度的清单,也不给你已经发现的问题,免得它顺着你的思路走。
|
|
65
|
+
|
|
66
|
+
**收回之后**(由你自己做):
|
|
67
|
+
- 合并去重:同一位置、同一原因的发现合成一条,级别取高的;
|
|
68
|
+
- 子 agent 列出的没读完的文件汇总到「未覆盖与待确认」;
|
|
69
|
+
- 影响面(第 5 步)仍由你自己做,再对合并后的发现做第 6 步复核(独立复核时另派子 agent,不交回给找出它的那个)。
|
|
70
|
+
|
|
71
|
+
报告「审查方式」写「深审,分 <K> 个角度并行查找,独立复核」。
|
|
72
|
+
|
|
73
|
+
## 5. 未提交改动
|
|
43
74
|
|
|
44
75
|
审查工作区(dev 自审或用户说「我刚改完,帮我看看」):`git diff HEAD`(已暂存和未暂存一起);分支已有提交时再加 `git diff <main>...HEAD`。新建但未跟踪的文件用 `git status --short` 找出来逐个读。
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> 审查范围:{{分支对比 / 单文件 / 未提交改动}}:{{范围说明}} | 日期:{{YYYY-MM-DD}}
|
|
4
4
|
> 对比分支:`{{dev分支}}` ←→ `{{基线分支}}`(单文件 / 未提交改动两边都写当前分支)
|
|
5
|
-
> 审查方式:{{全量精读 / 分批精读(N 批,按风险排序)/ 部分摘要化}} | 清单来源:{{DocNexus / 项目文件 / skill 默认}}
|
|
6
|
-
> 参考产物:{{本分支的开发方案 / 交付说明 /
|
|
5
|
+
> 审查方式:{{快审 / 标准 / 深审}},{{全量精读 / 分批精读(N 批,按风险排序)/ 分 K 个角度并行查找 / 部分摘要化}},{{独立复核 / 自行复核}} | 清单来源:{{DocNexus / 项目文件 / skill 默认}}
|
|
6
|
+
> 参考产物:{{本分支的开发方案 / 交付说明 / 技术方案路径;复审时加「上次审查 v<N>(<日期>)」;没有写「无」}}
|
|
7
7
|
> DocNexus:{{沉淀后写 docnexus/library/<path>;未沉淀写「未沉淀」,未接入写「未接入」}}
|
|
8
8
|
|
|
9
9
|
## 结论摘要 *
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
|
|
35
35
|
## 优化建议(LOW)
|
|
36
36
|
|
|
37
|
+
<!-- 只写有实际收益的(性能、可维护性);风格类只在深审时写。快审写「无」 -->
|
|
38
|
+
|
|
37
39
|
| # | 位置 | 建议 |
|
|
38
40
|
|---|---|---|
|
|
39
41
|
|
|
@@ -55,4 +57,4 @@
|
|
|
55
57
|
|
|
56
58
|
## 未覆盖与待确认 *
|
|
57
59
|
|
|
58
|
-
{{分批审查时只扫过 --stat
|
|
60
|
+
{{分批审查时只扫过 --stat 的文件、复核后降级或待确认的发现、复核排除的条数、改动前就存在的问题、按审查深度没查的维度;都没有写「无」}}
|
|
@@ -7,7 +7,7 @@ workflow-run/docs/requirements/<需求名>/
|
|
|
7
7
|
├── meta.json # 需求名、绑定的分支、各产物(种类、版本、日期、wiki 发布记录、DocNexus 沉淀)
|
|
8
8
|
├── plan.md # 开发方案(dev,L 规模或用户要求时)
|
|
9
9
|
├── delivery.md # 交付说明(dev)
|
|
10
|
-
├── review.md # 审查报告(project-review
|
|
10
|
+
├── review.md # 审查报告(project-review);复审也写这个文件,上一版归档(见「落盘」)
|
|
11
11
|
├── tech-doc.md # 技术方案(create-tech-md-doc)
|
|
12
12
|
└── release-doc.md # 上线方案(create-release-md-doc)
|
|
13
13
|
```
|
|
@@ -41,10 +41,11 @@ node <skills>/workflow/scripts/requirement.mjs which
|
|
|
41
41
|
node <skills>/workflow/scripts/requirement.mjs path <plan|delivery|review|tech|release>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
- 输出的 `path` 就是要写的文件(相对项目根)。文档里引用产物(「参考产物」、上线方案引用技术方案等)写输出的 `cite`:默认模式下和 `path`
|
|
44
|
+
- 输出的 `path` 就是要写的文件(相对项目根)。文档里引用产物(「参考产物」、上线方案引用技术方案等)写输出的 `cite`:默认模式下和 `path` 相同;文档仓库模式下是「文档仓库名:仓库内路径」,各服务、各人的克隆里都一样。每种产物的文件名固定(`review.md`、`tech-doc.md` …),引用它的地方不会因为出了新版而失效。已有时,脚本先把当前版复制到 `workflow-run/tmp/old-artifacts/<需求名>/<种类>_v<N>.md`,输出的 `previous` 就是这份(复审对照上次的报告读它),`version` 是这次的版本号;然后把新版直接写到 `path`。
|
|
45
45
|
- 退出码 2:当前分支还没绑定需求,按上一节问用户需求名,`bind` 之后再运行一次。
|
|
46
46
|
- `wiki_record` 非空:上一版已经发布到 wiki。新版的**第一行原样写这条记录**,md-to-wiki 再发布时会更新同一个页面,不会新建页面。
|
|
47
47
|
- 写完运行 `requirement.mjs sync <path>`,更新 `meta.json`。md-to-wiki 发布后改了第一行的记录,或补写了 DocNexus 元信息行,也要再 sync 一次。
|
|
48
|
+
- sync 时新版内容和归档的上一版不同,才算新版本(`meta.json` 的 `versions` 加一);没写就放弃的话版本号不变。输出里 `archived` 非空时,告诉用户归档了哪些文件、在哪里(`workflow-run/tmp/old-artifacts/<需求名>/`,文档仓库模式是 `<需求名>/<项目名>/`,不提交),请用户确认不再需要后自己删除;**不要替用户删**。随代码或文档仓库提交过的旧版也能从 git 历史找回。
|
|
48
49
|
- sync 输出里 `commit` 非空:按下一节「文档仓库的提交」处理。
|
|
49
50
|
|
|
50
51
|
## 独立文档仓库(克隆到 workflow-run/)
|
|
@@ -63,9 +63,9 @@ export function collect(root, { allProjects = false } = {}) {
|
|
|
63
63
|
const k = KINDS.find((x) => x.kind === f.kind);
|
|
64
64
|
const text = fs.readFileSync(f.abs, 'utf8');
|
|
65
65
|
// 排序键:文档头里的日期 + 版本号(需求目录里的文件名不带日期)
|
|
66
|
-
out.push({ kind: k.kind, label: k.label, path: f.rel, cite: f.cite, requirement: f.slug, project: f.project, branch: parseBranch(text), date: `${headerDate(text)}#${String(f.version).padStart(3, '0')}`, mtime: fs.statSync(f.abs).mtimeMs, wiki: parseWiki(text) });
|
|
66
|
+
out.push({ kind: k.kind, label: k.label, path: f.rel, cite: f.cite, requirement: f.slug, project: f.project, version: f.version, branch: parseBranch(text), date: `${headerDate(text)}#${String(f.version).padStart(3, '0')}`, mtime: fs.statSync(f.abs).mtimeMs, wiki: parseWiki(text) });
|
|
67
67
|
}
|
|
68
|
-
//
|
|
68
|
+
// 同一天、无日期的文件按修改时间排
|
|
69
69
|
return out.sort(byDate);
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
// 用法(在项目根执行;--root 可指定项目根;--branch 缺省为当前 git 分支):
|
|
13
13
|
// requirement.mjs which [--branch b] # 当前分支绑定的需求;没绑定时给出建议名、最近的需求(文档仓库模式还列出其他项目的需求)
|
|
14
14
|
// requirement.mjs bind [--name <需求名>] [--branch b] [--base m] # 绑定(没有就新建);不给 --name 按分支名生成
|
|
15
|
-
// requirement.mjs path <plan|delivery|review|tech|release> [--branch b] #
|
|
16
|
-
//
|
|
15
|
+
// requirement.mjs path <plan|delivery|review|tech|release> [--branch b] # 本次落盘的文件路径(文件名固定;已有时上一版复制到
|
|
16
|
+
// # workflow-run/tmp/old-artifacts/<种类>_v<N>.md,输出 previous)
|
|
17
|
+
// requirement.mjs sync [<需求目录或其中的文件> | --all] # 按目录里的文件重建 meta.json;指定了目录 / 文件时确认新版本号、输出归档了哪些上一版;
|
|
18
|
+
// # 文档仓库模式下带上提交提示
|
|
17
19
|
// requirement.mjs pending [--branch b] # 文档仓库模式:当前需求在文档仓库里未提交的改动与建议的提交命令
|
|
18
20
|
// requirement.mjs list # 所有需求(名称、分支、产物数、更新时间)
|
|
19
21
|
// requirement.mjs config [--project <项目名>] [--commit-prompt on|off] [--local] # 查看 / 修改产物配置
|
|
@@ -34,7 +36,7 @@ export const IGNORE_LINE = `${HOME}/docs/`;
|
|
|
34
36
|
export const homeDir = (root) => path.join(root, HOME);
|
|
35
37
|
// workflow-run/ 自己是 git 仓库(团队文档仓库克隆到这里)= 独立文档仓库模式
|
|
36
38
|
export const homeIsRepo = (root) => fs.existsSync(path.join(root, HOME, '.git'));
|
|
37
|
-
// 产物种类 →
|
|
39
|
+
// 产物种类 → 需求目录里的文件名(只放最新版 <base>.md;版本号在 meta.json 的 versions,旧版归档为 <base>_v<N>.md)
|
|
38
40
|
export const FILES = { plan: 'plan', delivery: 'delivery', review: 'review', tech: 'tech-doc', release: 'release-doc' };
|
|
39
41
|
const LABELS = { plan: '开发方案', delivery: '交付说明', review: '审查报告', tech: '技术方案', release: '上线方案' };
|
|
40
42
|
// 安装后的 skills 目录(安装器从安装包调用本脚本时,到项目里找画像)
|
|
@@ -181,11 +183,12 @@ export function artifactFiles(root, { allProjects = false } = {}) {
|
|
|
181
183
|
: requirements(root, L).map((r) => ({ abs: r.abs, slug: r.slug, project: L.project }));
|
|
182
184
|
const out = [];
|
|
183
185
|
for (const d of dirs) {
|
|
186
|
+
const versions = versionsOf(d.abs);
|
|
184
187
|
for (const name of fs.readdirSync(d.abs)) {
|
|
185
188
|
const p = parseFile(name);
|
|
186
189
|
if (!p) continue;
|
|
187
190
|
const abs = path.join(d.abs, name);
|
|
188
|
-
out.push({ abs, rel: relOf(root, abs), cite: L.ref(abs), slug: d.slug, project: d.project, ...p });
|
|
191
|
+
out.push({ abs, rel: relOf(root, abs), cite: L.ref(abs), slug: d.slug, project: d.project, ...p, ...(name === `${FILES[p.kind]}.md` ? { version: versions[p.kind] ?? 1 } : {}) });
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
return out;
|
|
@@ -198,16 +201,36 @@ export function find(root, branch, L = layout(root)) {
|
|
|
198
201
|
return hits[0] ?? null;
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
// 同种产物只留最新版,文件名固定(review.md、tech-doc.md …),文档里的引用不会失效。版本号记在 meta.json 的 versions。
|
|
205
|
+
// path 时把当前版复制到 workflow-run/tmp/old-artifacts/<需求目录(相对 requirements/)>/<种类>_v<N>.md(不提交),
|
|
206
|
+
// 写完 sync <path> 时新版内容和这份副本不同才算新版本(versions 加一);没写就放弃的话版本号不变
|
|
207
|
+
export const OLD_DIR = `${TMP_DIR}/old-artifacts`;
|
|
208
|
+
const archivePath = (root, dirAbs, kind, v) => path.join(root, OLD_DIR, path.relative(path.join(root, REQ_DIR), dirAbs), `${FILES[kind]}_v${v}.md`);
|
|
209
|
+
const versionsOf = (dirAbs) => readJson(path.join(dirAbs, 'meta.json'))?.versions ?? {};
|
|
210
|
+
|
|
201
211
|
// 按目录里的文件重建 meta.json(保留需求名、分支、创建时间)
|
|
202
|
-
|
|
212
|
+
// bump:写完新版后(sync <path>)确认新版本——和 path 时归档的上一版副本内容不同就把版本号加一,返回归档了哪些
|
|
213
|
+
export function sync(root, dirRel, { bump = false } = {}) {
|
|
203
214
|
const dir = path.resolve(root, dirRel);
|
|
204
215
|
const f = path.join(dir, 'meta.json');
|
|
205
216
|
const old = readJson(f) ?? {};
|
|
217
|
+
const versions = { ...(old.versions ?? {}) };
|
|
218
|
+
const archived = [];
|
|
219
|
+
if (bump) {
|
|
220
|
+
for (const kind of Object.keys(FILES)) {
|
|
221
|
+
const cur = path.join(dir, `${FILES[kind]}.md`);
|
|
222
|
+
const v = versions[kind] ?? 1;
|
|
223
|
+
const arc = archivePath(root, dir, kind, v);
|
|
224
|
+
if (!fs.existsSync(cur) || !fs.existsSync(arc) || fs.readFileSync(arc).equals(fs.readFileSync(cur))) continue;
|
|
225
|
+
versions[kind] = v + 1;
|
|
226
|
+
archived.push({ kind, version: v, file: relOf(root, arc) });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
206
229
|
const artifacts = fs.readdirSync(dir).map((name) => {
|
|
207
230
|
const p = parseFile(name);
|
|
208
231
|
if (!p) return null;
|
|
209
232
|
const text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
210
|
-
return { kind: p.kind, version: p.version, file: name, branch: parseBranch(text), date: headerDate(text), wiki: parseWiki(text), docnexus: parseDocnexus(text) };
|
|
233
|
+
return { kind: p.kind, version: name === `${FILES[p.kind]}.md` ? versions[p.kind] ?? 1 : p.version, file: name, branch: parseBranch(text), date: headerDate(text), wiki: parseWiki(text), docnexus: parseDocnexus(text) };
|
|
211
234
|
}).filter(Boolean).sort((a, b) => Object.keys(FILES).indexOf(a.kind) - Object.keys(FILES).indexOf(b.kind) || a.version - b.version);
|
|
212
235
|
const branches = [...new Set([...(old.branches ?? []), ...artifacts.map((a) => a.branch).filter(Boolean)])];
|
|
213
236
|
const latest = Object.fromEntries(Object.keys(FILES).map((k) => [k, artifacts.filter((a) => a.kind === k).at(-1)?.file ?? null]));
|
|
@@ -215,11 +238,11 @@ export function sync(root, dirRel) {
|
|
|
215
238
|
const changed = JSON.stringify(old.artifacts ?? null) !== JSON.stringify(artifacts) || JSON.stringify(old.branches ?? null) !== JSON.stringify(branches);
|
|
216
239
|
const meta = {
|
|
217
240
|
name: old.name ?? path.basename(dir), named_by: old.named_by ?? 'user', ...(old.project ? { project: old.project } : {}), branches, base: old.base ?? null,
|
|
218
|
-
created: old.created ?? now(), updated: changed || !old.updated ? now() : old.updated, latest, artifacts,
|
|
241
|
+
created: old.created ?? now(), updated: changed || !old.updated ? now() : old.updated, latest, ...(Object.keys(versions).length ? { versions } : {}), artifacts,
|
|
219
242
|
};
|
|
220
243
|
const out = `${JSON.stringify(meta, null, 2)}\n`;
|
|
221
244
|
if (!fs.existsSync(f) || fs.readFileSync(f, 'utf8') !== out) fs.writeFileSync(f, out);
|
|
222
|
-
return meta;
|
|
245
|
+
return bump ? { ...meta, archived } : meta;
|
|
223
246
|
}
|
|
224
247
|
|
|
225
248
|
const notReady = (L) => ({ ok: false, mode: L.mode, ...(L.need_project ? { need_project: true, suggested_project: L.suggested_project } : {}), hint: L.hint });
|
|
@@ -267,20 +290,25 @@ export function bind(root, { name = null, branch, base = null }) {
|
|
|
267
290
|
return { ok: true, mode: L.mode, created: !existedHere, joined: !!target && !existedHere && L.mode === 'repo', name: meta.name, named_by: meta.named_by, dir: rel, ...(L.mode === 'repo' ? { project: L.project } : {}), branches: sync(root, rel).branches };
|
|
268
291
|
}
|
|
269
292
|
|
|
270
|
-
//
|
|
293
|
+
// 本次落盘路径:文件名固定(review.md …),已有时先把当前版复制到 tmp/old-artifacts/<种类>_v<N>.md(previous,复审对照用),
|
|
294
|
+
// 并带上它第一行的 wiki 发布记录;新版直接写到 path(覆盖当前版),写完 sync <path> 确认版本号
|
|
271
295
|
export function nextPath(root, kind, branch) {
|
|
272
296
|
if (!FILES[kind]) return { code: 1, out: { ok: false, error: `未知产物种类「${kind}」:${Object.keys(FILES).join(' / ')}` } };
|
|
273
297
|
const L = layout(root);
|
|
274
298
|
if (!L.ok) return { code: 2, out: notReady(L) };
|
|
275
299
|
const r = find(root, branch, L);
|
|
276
300
|
if (!r) return { code: 2, out: { ok: false, ...which(root, branch) } };
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
301
|
+
const abs = path.join(r.abs, `${FILES[kind]}.md`);
|
|
302
|
+
let v = 1; let previous = null; let record = null;
|
|
303
|
+
if (fs.existsSync(abs)) {
|
|
304
|
+
const cur = versionsOf(r.abs)[kind] ?? 1;
|
|
305
|
+
const arc = archivePath(root, r.abs, kind, cur);
|
|
306
|
+
fs.mkdirSync(path.dirname(arc), { recursive: true });
|
|
307
|
+
fs.copyFileSync(abs, arc);
|
|
308
|
+
v = cur + 1; previous = relOf(root, arc);
|
|
309
|
+
record = /^<!-- md-to-wiki: [^\n]*-->/.exec(fs.readFileSync(abs, 'utf8'))?.[0] ?? null;
|
|
310
|
+
}
|
|
311
|
+
return { code: 0, out: { ok: true, mode: L.mode, name: r.name, dir: r.dir, path: relOf(root, abs), cite: L.ref(abs), version: v, previous, wiki_record: record } };
|
|
284
312
|
}
|
|
285
313
|
|
|
286
314
|
export function list(root) {
|
|
@@ -390,8 +418,12 @@ function main(argv) {
|
|
|
390
418
|
target = requirements(root, L).find((r) => abs === r.abs || abs.startsWith(r.abs + path.sep)) ?? null;
|
|
391
419
|
} else target = find(root, branch, L);
|
|
392
420
|
if (!target) { console.error('不在当前项目的需求目录下,或当前分支没有绑定需求'); return 1; }
|
|
393
|
-
const m = sync(root, target.dir);
|
|
394
|
-
const out = { dir: target.dir, name: m.name, artifacts: m.artifacts.length, latest: m.latest };
|
|
421
|
+
const m = sync(root, target.dir, { bump: true });
|
|
422
|
+
const out = { dir: target.dir, name: m.name, artifacts: m.artifacts.length, latest: m.latest, versions: m.versions ?? {} };
|
|
423
|
+
if (m.archived.length) {
|
|
424
|
+
const to = path.posix.dirname(m.archived[0].file);
|
|
425
|
+
out.archived = { files: m.archived, dir: to, hint: `上一版已归档到 ${to}/(不提交),目录里只留最新版。告诉用户归档了哪些文件,请用户确认不再需要后自己删除;不要替用户删` };
|
|
426
|
+
}
|
|
395
427
|
// 文档仓库模式:写完产物后提示提交(由用户确认;artifacts.commit_prompt 关闭时不提示)
|
|
396
428
|
if (L.mode === 'repo') {
|
|
397
429
|
const pd = pending(root, branch, target.dir);
|
|
@@ -36,7 +36,7 @@ export function viewData(root) {
|
|
|
36
36
|
if (a.branch) g.branches.add(a.branch);
|
|
37
37
|
if (a.project) g.projects.add(a.project);
|
|
38
38
|
const text = fs.readFileSync(path.join(root, a.path), 'utf8');
|
|
39
|
-
const item = { kind: a.kind, label: a.label, path: a.path, cite: a.cite ?? a.path, project: a.project ?? null, file: a.project && L.mode === 'repo' ? `${a.project}/${path.basename(a.path)}` : path.basename(a.path), date: (a.date || '').slice(0, 10) || null, branch: a.branch, wiki: a.wiki, text };
|
|
39
|
+
const item = { kind: a.kind, label: a.label, path: a.path, cite: a.cite ?? a.path, project: a.project ?? null, file: a.project && L.mode === 'repo' ? `${a.project}/${path.basename(a.path)}` : path.basename(a.path), date: (a.date || '').slice(0, 10) || null, version: a.version ?? 1, branch: a.branch, wiki: a.wiki, text };
|
|
40
40
|
if (a.kind === 'review') item.review = parseReview(text);
|
|
41
41
|
if (a.kind === 'delivery') item.delivery = parseDelivery(text);
|
|
42
42
|
if (a.kind === 'review' || a.kind === 'delivery') item.docnexus = captureState(text);
|
|
@@ -214,8 +214,8 @@ function renderList() {
|
|
|
214
214
|
function reviewBlock(r) {
|
|
215
215
|
const rs = r.artifacts.filter((a) => a.kind === 'review');
|
|
216
216
|
if (!rs.length) return '';
|
|
217
|
-
const row = (a
|
|
218
|
-
return '<tr><td><a href="#" data-doc="' + esc(a.path) + '">' + esc(a.file) + '</a></td><td>' + esc(a.date || '') + '</td><td>' + (
|
|
217
|
+
const row = (a) => { const v = a.review; const f = v.follow_up;
|
|
218
|
+
return '<tr><td><a href="#" data-doc="' + esc(a.path) + '">' + esc(a.file) + '</a></td><td>' + esc(a.date || '') + '</td><td>' + (v.is_rereview ? '复审' : '首次') + '</td>'
|
|
219
219
|
+ '<td><span class="lv c">' + v.levels.CRITICAL + '</span> <span class="lv h">' + v.levels.HIGH + '</span> ' + v.levels.MEDIUM + ' / ' + v.levels.LOW + '</td>'
|
|
220
220
|
+ '<td>✅ ' + v.verified + ' · ⚠ ' + v.pending + '</td><td>' + (v.is_rereview ? '<span class="lv f">已修复 ' + f.fixed + '</span> 仍存在 ' + f.open + ' · 不再适用 ' + f.obsolete : '—') + '</td><td>' + esc(a.docnexus === 'captured' ? '已沉淀' : a.docnexus === 'not_captured' ? '未沉淀' : a.docnexus === 'not_connected' ? '未接入' : '—') + '</td></tr>'; };
|
|
221
221
|
return '<section class="block"><h3>审查记录</h3><table><thead><tr><th>报告</th><th>日期</th><th>轮次</th><th>CRITICAL · HIGH · MEDIUM / LOW</th><th>复核</th><th>与上次对比</th><th>DocNexus</th></tr></thead><tbody>' + rs.map(row).join('') + '</tbody></table></section>';
|
|
@@ -253,7 +253,7 @@ function select(id, docPath) {
|
|
|
253
253
|
try { history.replaceState(null, '', '#' + encodeURIComponent(id)); } catch {}
|
|
254
254
|
const latest = (k) => r.artifacts.filter((a) => a.kind === k).at(-1);
|
|
255
255
|
const pipeline = '<div class="pipeline">' + D.kinds.map((k) => { const a = latest(k.kind); const n = r.stages[k.kind];
|
|
256
|
-
return '<div class="stage' + (n ? ' y' : '') + '"><div class="t">' + k.label + '</div><div class="c">' + (n ? (
|
|
256
|
+
return '<div class="stage' + (n ? ' y' : '') + '"><div class="t">' + k.label + '</div><div class="c">' + (n ? (a && a.version > 1 ? 'v' + a.version : '✓') + (a && a.date ? ' · ' + a.date : '') : '—') + '</div>' + (a && a.wiki ? '<div class="t">wiki ' + (a.wiki.url ? '<a href="' + esc(a.wiki.url) + '" target="_blank" rel="noopener">pageId ' + esc(a.wiki.pageId) + '</a>' : 'pageId ' + esc(a.wiki.pageId)) + '</div>' : '') + '</div>'; }).join('') + '</div>';
|
|
257
257
|
const order = D.kinds.map((k) => k.kind);
|
|
258
258
|
const files = '<div class="files">' + r.artifacts.slice().sort((x, y) => order.indexOf(x.kind) - order.indexOf(y.kind)).map((a) => '<button data-doc="' + esc(a.path) + '">' + esc(a.label) + ' · ' + esc(a.file) + '</button>').join('') + '</div>';
|
|
259
259
|
$('#main').innerHTML = overview() + '<h2>' + esc(r.name) + '</h2><div class="sub">' + esc(r.dir ?? '') + (r.projects.length ? ' · 项目 ' + esc(r.projects.join('、')) : '') + ' · 分支 ' + esc(r.branches.join('、') || '无') + (r.named_by === 'branch' ? ' · <span class="tag">需求名由分支名生成,可在 meta.json 改</span>' : '') + '</div>'
|