feat: 发布 comment-review 1.0.0
确定性注释审查 harness(cmr),程序决定审什么、agent 负责推理: - 轻量状态机提取注释(Java/TS/JS/Vue/SQL/YAML/Shell/Python),过程性模式分级 P0/P1/P2/INFO - 分层规则(CLI/项目/用户/内置)、全库 scan 与 Git 增量 diff(工作区/分支/提交) - 悬空引用两级判定:确认失效 P0(附近似候选与查找依据)与待核实 P1(外部路径/构建产物/跨仓裸文件名) - testdata/corpus 56 条人工判定样例库 + 记分卡回归门禁;npm test 30/30
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
@@ -0,0 +1,80 @@
|
||||
# comment-review (cmr)
|
||||
|
||||
确定性注释审查 Harness。提取注释、检测过程性污染(日期、版本、评审历程、人物归因)、输出分级审查规范,由宿主 agent(ZCode/Codex/Claude Code)完成最终判别。
|
||||
|
||||
架构参考 [alibaba/open-code-review](https://github.com/alibaba/open-code-review) 的 Delegation Mode:**程序决定审什么,agent 负责推理**。
|
||||
|
||||
## 判别准绳
|
||||
|
||||
> 注释回答"现在为什么是这样",不叙述"怎么变成现在这样的"。
|
||||
> 变更日期、版本历程、驳回定稿、人物归因的合法归宿是 git 提交消息。
|
||||
> 例外:表达业务规则生效条件的日期("自 2026 年 3 月起…")是约束,保留但改写为无过程痕迹的陈述。
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
cmr scan # 全库注释审计
|
||||
cmr diff # 工作区新增/变化注释
|
||||
cmr diff --from main --to feat/x # 分支增量
|
||||
cmr diff --commit <hash> # 提交相对第一父提交的增量
|
||||
cmr rules <file>... # 查看规则解析
|
||||
# 通用旗标:--format text|json --min-severity p0|p1|p2|all --rule <file> --path <dir>
|
||||
```
|
||||
|
||||
未全局安装时,将 `cmr` 替换为 `node bin/cmr.js`。运行 `npm test` 执行回归测试;无需安装第三方依赖,Git 相关用例使用独立临时仓库。
|
||||
|
||||
`scan` 可用于没有 Git 的目录,`diff` 需要 Git 仓库。工作区增量比较当前磁盘内容与 HEAD,并包含未跟踪文件;无首次提交时以空内容为基线。分支范围使用 `merge-base(from, to)` 到 `to`,提交模式使用第一父提交到目标提交。历史模式从 Git 对象读取源码和引用文件清单,不受工作区脏文件影响。
|
||||
|
||||
`--path` 限定文件或子目录,引用检查仍使用整个根目录的清单。diff 从子目录运行时自动定位 Git 根;显式相对路径仍以调用目录为准。严重级别大小写均可,`p1` 表示保留 P0 和 P1,`all` 包含 INFO。参数错误退出码为 2,读取/配置/Git 错误为 1;正常生成报告为 0,即使存在候选也不作为 CI 失败门禁。
|
||||
|
||||
## 分级语义
|
||||
|
||||
| 级别 | 判定 | 处置 |
|
||||
|------|------|------|
|
||||
| P0 | 日期+过程信号 / 修订记录标题 / 强过程动词 / 确认失效引用 | 删除候选(git 已有历史) |
|
||||
| P1 | 人物归因 / 新旧对比 / 方案演变 / 版本+过程 / 引用待核实 | 拆分改写:留约束、抹过程;引用按证据核实 |
|
||||
| P2 | 仅日期/版本 | 判别业务生效日 vs 变更日期 |
|
||||
| INFO | TODO/FIXME | 台账(含截止日期) |
|
||||
|
||||
## 规则分层(同 ocr 语义)
|
||||
|
||||
CLI `--rule` > 项目 `.commentreview/rule.json` > 用户 `~/.commentreview/rule.json` > 内置。
|
||||
Schema:`{"rules":[{"path":"<glob>","rule":"<markdown>","merge_system_rule":true}],"excludes":["generated/**"]}`,首个匹配 glob 生效。
|
||||
|
||||
按优先级选择首个存在的配置文件,不跨文件合并;项目配置向父目录查找。历史审查也使用当前配置。显式指定的规则文件不存在、JSON 或字段类型错误时会报错。
|
||||
|
||||
glob 支持 `**`、`*`、`?`,忽略大小写,不支持否定或花括号展开。`excludes` 相对于扫描根目录,仅排除审查候选,不移除引用目标。`rules` 输出匹配来源及完整规则文本,也支持 `--format json`。自定义规则文本交给宿主 agent,不改变内置正则候选检测器。
|
||||
|
||||
## 悬空引用:确认失效 vs 待核实
|
||||
|
||||
"本地找不到"不等于"注释错了"。引用分两级,每条候选附**查找依据**(仓库相对路径、源文件同目录、全库同名)与**近似候选**:
|
||||
|
||||
- **确认失效(P0)**:仅在有结构性证据时判定——同名文件在别处、同干不同扩展名、文件名仅数字差异(疑似移动/改名),或目标目录存在而文件不在(疑似删除)。
|
||||
- **待核实(P1,`unverified_ref`)**:仓库外绝对路径与 `~` 家目录、构建/依赖目录(`dist`、`lib`、`node_modules` 等,本就不在清单内)、跨仓或仓库外的裸文件名(无法区分外部文件与已删除文件)、中英混排的疑似描述性文字。可选配置、生成产物、跨仓引用完全合法,由 agent 按证据定夺。
|
||||
|
||||
带目录路径按根目录或源文件目录解析,裸文件名允许全库同名匹配;URL 和引号内示例忽略;不校验章节与远端资源。
|
||||
|
||||
## 判定基线(人工样例库)
|
||||
|
||||
`testdata/corpus/` 收录 56 条来自真实仓库审查的人工判定样例,分四类:**过程记录**(必须检出且级别一致)、**业务约束**/**正常说明**(不得误报)、**待核实**(必须以 P1 附证据列出)。`test/corpus.test.js` 输出分类记分卡并作为回归门禁:调整检测规则前后运行 `npm test`,防止修一类注释误伤另一类。判定先于工具输出记录,新增样例流程见 `testdata/corpus/README.md`;样例放在 `test/` 之外,避免被测试执行器误当作测试。
|
||||
|
||||
以下上下文已按样例库裁决自动豁免:「兼容旧版 X」(存活兼容要求);旧版/新版锚定业务对象(流程定义/节点/入口/路由/浏览器/钉钉等外部产品);字段枚举文档("外文版本:1是,0否"——`版本` 为更长业务词的尾部);格式示例日期("例如 2023年7月"、"格式 YYYY-MM,如 2026-08");库能力版本("3.5.0 版本 增加 xxx");流程位置的"后续会签";角色/操作词"副总/副总经理/汇总/手工/人工"(非人名称谓);用法行尖括号占位符 `<flow.json>` 不算文件引用。
|
||||
|
||||
## 已知边界
|
||||
|
||||
- 提取器是轻量状态机,不含完整语法树:支持转义引号、多行模板字符串、Java 文本块和常见 Python 三引号文档;JS 正则、模板插值内注释、Vue/JSX 混合语法、Shell heredoc、YAML 块标量仍需人工核对。独立 Python 三引号按文档候选处理,不判定其是否处于合法 docstring 位置
|
||||
- 增量按每个文件的注释文本及出现次数比较,移动注释不算新增;纯重命名按新文件处理。注释内容不变但所引用文档被删时,应运行 `scan` 检查全库
|
||||
- 跳过符号链接/目录联接和常见构建目录,避免循环遍历。统计 files 为实际读取的支持语言文件数;读取失败直接报错,不输出假成功
|
||||
- 悬空引用两级判定与证据输出见上文专节;正则候选仍不校验章节与远端资源
|
||||
- CRUD 动词(新增/修改/删除)单独出现视为接口描述不告警,仅作组合证据
|
||||
- 驳回/评审等词在 OA/工作流域是业务术语,已从过程词表移除
|
||||
|
||||
## 部署
|
||||
|
||||
```bash
|
||||
npm i -g <本工程路径> # 本机
|
||||
# dev: scp 工程到 ~/work/tool/comment-review 后 npm i -g ~/work/tool/comment-review
|
||||
```
|
||||
|
||||
配套 skill:`~/.agents/skills/comment-review/SKILL.md`(判别流程与输出格式)。
|
||||
审查报告样例:`reports/*.md`。
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import { runCLI } from "../src/cli.js";
|
||||
|
||||
runCLI(process.argv.slice(2)).then(
|
||||
(code) => process.exit(code ?? 0),
|
||||
(err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "comment-review",
|
||||
"version": "1.0.0",
|
||||
"description": "Deterministic comment-review harness. Extracts comments, detects process-narration pollution (dates, versions, review history, attribution), and emits a review spec for host agents. Architecture modeled on alibaba/open-code-review delegation mode: the program decides WHAT to review, the agent reasons.",
|
||||
"bin": {
|
||||
"cmr": "bin/cmr.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "node --test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# comment-review 工程审查与优化
|
||||
|
||||
本次审查已修复会造成空报告、错误版本审查和注释误提取的问题,并增加可执行回归测试。工程仍定位为确定性候选提取工具,候选是否属于业务约束由宿主 agent 判断。
|
||||
|
||||
## 审查范围与依据
|
||||
|
||||
- 日期:2026-09-14;环境:Windows、Node.js v22.21.0。
|
||||
- 原目录未初始化 Git。在临时副本建立基线,使用 OCR v1.12.0 的 `delegate preview`、`delegate rule` 完成文件筛选和规则解析,再由当前 agent 审查。
|
||||
- 没有项目 `.opencodereview/rule.json`,使用内置 JS 和 package.json 两个规则组;后续可按工程约定添加项目规则。
|
||||
- 覆盖原有 9 个实现/配置文件和新增 `src/git.js`;`src/glob.js`、`src/spec.js`、`bin/cmr.js` 未发现本轮需要修改的问题。README 和测试被 OCR 默认排除,但分别进行了说明核对和执行验证。
|
||||
- 以下等级表示工程缺陷影响,不等同于 cmr 输出的注释候选 P0/P1/P2。
|
||||
|
||||
## 已修复问题
|
||||
|
||||
| 影响 | 证据与问题 | 修复位置及结果 | 规则依据 |
|
||||
|---|---|---|---|
|
||||
| 高 | `--min-severity p0` 查小写键,而映射表只有大写键;样例错误输出 0 个候选 | `src/scan.js:81`、`src/cli.js:45`:统一大小写并拒绝非法级别 | JS / Code Quality Checks |
|
||||
| 高 | 分支和提交模式只用 Git 取文件名,内容读取工作区;并把未变化的注释也交给审查 | `src/git.js:28`、`src/scan.js:103`:读取目标 Git 对象,按基线注释文本和次数排除未变化项;引用清单也取目标树 | JS / Code Quality Checks |
|
||||
| 高 | Git 文件名按换行拆分,中文文件名会被 Git 引号转义;无 HEAD 报错,新增同文注释被 Set 吞掉 | `src/git.js:17`、`src/git.js:56`:NUL 分隔、空基线和计数比较;支持中文空格路径及首次提交 | JS / Code Quality Checks |
|
||||
| 高 | `excludes` 被读取但未参与筛选,diff 忽略 `--path`;目录联接会重复递归扫描 | `src/scan.js:15`、`src/scan.js:74`:统一筛选,跳过符号链接/联接,稳定排序,统计实际读取文件;复用一次文件清单 | JS / Dead Code、Code Quality Checks |
|
||||
| 高 | 转义引号、多行模板字符串内容被误提取;Javadoc 类型始终为 block,Python 三引号先进入普通字符串分支 | `src/extract.js:33`:追踪转义与多行状态,正确识别文档块;properties/YAML 的值内标记不当注释 | JS / Dead Code、Code Quality Checks |
|
||||
| 中 | URL 被当成本地文件;显式错误路径能被其他目录的同名文件“补救”;一个真实日期文件会豁免整个注释的日期 | `src/scan.js:39`、`src/patterns.js:107`:先移除 URL,显式路径精确匹配,只豁免已解析的文件名 | JS / Code Quality Checks |
|
||||
| 中 | 单独 TODO 被提前过滤;中文称谓后的 ASCII 单词边界导致“张总:”漏检 | `src/patterns.js:107` 及 attribution 模式:恢复 INFO 台账,修正中文称谓匹配 | JS / Code Quality Checks |
|
||||
| 中 | 未知选项、错误格式和缺值被静默接受;rules 把选项值当文件;显式规则不存在时回退默认 | `src/cli.js:17`、`src/rules.js:27`:参数与配置校验,完整规则文本/JSON 输出,明确失败退出码 | JS / Async Handling Standards、Code Quality Checks |
|
||||
|
||||
## 验证
|
||||
|
||||
- 修复前新增首批 22 个回归测试:21 个失败、1 个通过,复现上述缺陷。
|
||||
- 修复后 `npm test`:**28 / 28 通过**,包括临时 Git 仓库集成测试;覆盖脏工作区、首次提交、merge-base、删除文件、仅代码变化、路径范围、规则配置和注释提取。
|
||||
- 原始 `ProcessNoise.java` 保持不变。`node bin/cmr.js scan --path test/fixtures --format json`:1 个文件、18 段注释、12 个候选,P0=3、P1=8、P2=0、INFO=1。真实日期文档、流程实例仍在流转、超时现状、CRUD 描述及 IP 示例未被列为候选。
|
||||
- 在临时副本对最终实现与原始基线执行 `git diff --check`:通过;Git 的 LF/CRLF 提示不属于空白错误。
|
||||
- 未添加运行时第三方依赖。测试入口为 Node 内置 test runner。尚未在 Node 18 或 Linux 上执行兼容性验证。
|
||||
|
||||
## 使用边界
|
||||
|
||||
- 轻量状态机不能完整理解 JS 正则、模板插值、Vue/JSX 混合语法、Shell heredoc 和 YAML 块标量,详见 README。
|
||||
- 悬空引用仍是候选检查,不能把“本地未找到”直接等同于注释错误。例如自身 `src/rules.js` 的可选配置路径会被列入候选,应由 agent 保留;工具不会自动删除注释。
|
||||
- diff 比较文本及次数,纯重命名按新文件处理。只有引用目标被删除而注释未改时,使用全量 scan 检查。
|
||||
- 历史模式使用当前规则配置;退出码 0 表示报告生成成功,不表示没有候选。
|
||||
|
||||
具体命令、规则 schema 和排除语义已同步到 README。
|
||||
@@ -0,0 +1,125 @@
|
||||
# Comment Review Spec (scan)
|
||||
|
||||
- repo: C:\Development\Code\zhgl\zhgl-front
|
||||
- rules: project (C:\Development\Code\zhgl\zhgl-front\.commentreview\rule.json)
|
||||
- files: 315 | comments: 2873 | candidates: 12
|
||||
- severity: P0=2 P1=10 P2=0 INFO=0
|
||||
|
||||
### Comment Rule Group 1: system / **
|
||||
|
||||
Applies to:
|
||||
- src\lang\en_US.ts
|
||||
- src\lang\zh_CN.ts
|
||||
- src\store\modules\permission.ts
|
||||
- src\types\vue-office.d.ts
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
### Comment Rule Group 2: project / src/utils/**
|
||||
|
||||
Applies to:
|
||||
- src\utils\notification.ts
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## 工具函数注释要求
|
||||
- JSDoc 写清输入约束(空值、边界、格式)与副作用;纯函数的显然行为不必注释
|
||||
|
||||
### Comment Rule Group 3: project / src/views/**/*.vue
|
||||
|
||||
Applies to:
|
||||
- src\views\contract\paymentForm.vue
|
||||
- src\views\documents\receiveForm.vue
|
||||
- src\views\documents\sendForm.vue
|
||||
- src\views\login.vue
|
||||
- src\views\vehicle\applyList.vue
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## 页面/组件注释要求
|
||||
- 组件顶部注释说明页面职责与关键交互约束(权限、状态联动),一段以内
|
||||
- 复杂表单校验规则、跨组件通信处允许行内注释解释动机
|
||||
|
||||
## Candidates
|
||||
|
||||
### 高危(纯过程记录,删除候选) — 2 处
|
||||
|
||||
- `src\store\modules\permission.ts:13-13` [悬空引用:匹配views里面所有的.vue 不存在] 匹配views里面所有的.vue文件
|
||||
- 挂接: const modules = import.meta.glob('./../../views/**/*.vue');
|
||||
- `src\types\vue-office.d.ts:3-4` [悬空引用:lib/index.js 不存在] vue-office 纯前端预览组件类型声明 pnpm 跳过了 postinstall 脚本,lib/index.js 未生成,故直接引用 v3(对应 Vue3) 构建产物
|
||||
|
||||
### 中危(混合/演变叙述,拆分改写) — 10 处
|
||||
|
||||
- `src\lang\en_US.ts:4-4` [新旧对比/历史因果叙述] 兼容旧版静态路由 key(避免 meta.title 使用 dashboard/document 时无法命中翻译)
|
||||
- `src\lang\zh_CN.ts:6-6` [新旧对比/历史因果叙述] 兼容旧版静态路由 key(避免 meta.title 使用 dashboard/document 时无法命中翻译)
|
||||
- `src\utils\notification.ts:45-45` [新旧对比/历史因果叙述] 兼容旧版浏览器,避免静默报错阻塞
|
||||
- `src\utils\notification.ts:86-86` [新旧对比/历史因果叙述] 极少数浏览器(如旧版 360)不支持 Promise 形式,降级尝试回调版
|
||||
- `src\views\contract\paymentForm.vue:715-715` [新旧对比/历史因果叙述] 修改已关联的合同名称即切换到手工模式,原台账字段不再适用。
|
||||
- `src\views\documents\receiveForm.vue:712-712` [新旧对比/历史因果叙述] 旧版据此选择部门承办或副总办理;新版据此决定合并办理节点是否包含副总。
|
||||
- `src\views\documents\receiveForm.vue:717-719` [新旧对比/历史因果叙述] 保存业务数据(草稿/编辑模式)。审批中的业务回写走专用办理接口 (completeCirculateReceive / completeLeaderReviewReceive / completeDeptHandleReceive / approvalSaveAttachmentReceive), 不再整表单提交。…
|
||||
- 挂接: const saveBusinessData = async () => {
|
||||
- `src\views\documents\sendForm.vue:706-707` [新旧对比/历史因果叙述] 保存业务数据(草稿/编辑模式)。审批中的业务回写走专用接口(approvalSaveDocNo/approvalSaveAttachment), 不再整表单提交。
|
||||
- 挂接: const saveBusinessData = async () => {
|
||||
- `src\views\login.vue:188-189` [新旧对比/历史因果叙述] 后端已关闭多租户,不再请求租户列表接口 initTenantList();
|
||||
- `src\views\vehicle\applyList.vue:119-119` [新旧对比/历史因果叙述] 搜索和切换页签会主动刷新;此处不再监听分页变量,避免重复请求。
|
||||
- 挂接: const switchTab = (tab: string) => {
|
||||
|
||||
## Pattern Distribution
|
||||
|
||||
- old_new: 10
|
||||
- dangling_ref: 2
|
||||
- crud_verb: 1
|
||||
@@ -0,0 +1,274 @@
|
||||
# Comment Review Spec (scan)
|
||||
|
||||
- repo: C:\Development\Code\zhgl\zhgl-server
|
||||
- rules: project (C:\Development\Code\zhgl\zhgl-server\.commentreview\rule.json)
|
||||
- files: 1030 | comments: 8386 | candidates: 55
|
||||
- severity: P0=5 P1=33 P2=17 INFO=0
|
||||
|
||||
### Comment Rule Group 1: project / **
|
||||
|
||||
Applies to:
|
||||
- .gitlab-ci.yml
|
||||
- script\bin\ts.sh
|
||||
- zhgl-admin\src\main\resources\application.yml
|
||||
- zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java
|
||||
- zhgl-common\zhgl-common-mybatis\src\main\java\com\zhgl\common\mybatis\aspect\DataPermissionPointcut.java
|
||||
- zhgl-common\zhgl-common-social\src\main\java\me\zhyd\oauth\request\AuthDingTalkV2Request.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\support\OaWorkflowNodes.java
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## 过程性表述判定口径(全局,与分层要求叠加)
|
||||
- 判别二分:时间/人物/版本指向代码变更过程(删或改写),还是业务规则与外部对象(保留)。外部对象版本锚定保留(库版本如 MP 3.5.0、流程定义发布版本、发布脚本版本、发布基线头部元数据);代码自身版本演变(「v2.1 起存在」「第二阶段/阶段 3/Phase/T3」)删编号、留兼容行为语义
|
||||
- 措辞黑名单(复扫 grep):(原先|此前|曾致|改为[:]|不再(加|重复|连坐|逐处|自行)|取代原先|[0-9]{1,2}/[0-9]{1,2} 合入|R[0-9]+ 修正|早期版本|反哺|实测[::]|自 [0-9a-f]{7,} 并入)
|
||||
- 改写公式:「不再 X」→「不 X/避免 X」;「取代原先 X」→「免去 X」;历史因果→可能因果(「历史上因 X 累积过多」→「可能因 X 累积多条」);实测性能数字删、由其支撑的超时/阈值取值约束保留
|
||||
- 悬空引用按 P0 处置:注释引用的文档/文件/版本号已不存在时,删除或改写为实锚点
|
||||
- 测试代码特判:「本轮/本次」=当前运行语义,保留;「偶发 N 秒」=超时取值依据,保留
|
||||
- 豁免:vendor 第三方目录、SQL 数据字面量日期(create_time 等业务数据)、错误信息字符串、带日期的文档文件名(按实际文件名引用)
|
||||
- 改既有注释三步:删失效过程叙事、保留仍成立的技术约束、不追加「历史上曾经」
|
||||
|
||||
### Comment Rule Group 2: project / script/sql/**
|
||||
|
||||
Applies to:
|
||||
- script\sql\release\1.0.0\01_system.sql
|
||||
- script\sql\release\1.0.0\02_snailjob.sql
|
||||
- script\sql\release\1.0.0\03_workflow.sql
|
||||
- script\sql\release\1.0.0\04_document.sql
|
||||
- script\sql\release\1.0.0\05_seal.sql
|
||||
- script\sql\release\1.1.0\V1.1.0_001__contract.sql
|
||||
- script\sql\release\1.1.0\V1.1.0_002__supplies.sql
|
||||
- script\sql\release\1.1.0\V1.1.0_003__vehicle.sql
|
||||
- script\sql\release\1.1.0\V1.1.0_004__schema_history.sql
|
||||
- script\sql\release\1.1.0\V1.1.0_005__document_seal.sql
|
||||
- script\sql\release\1.1.1\V1.1.1_001__document_send_version.sql
|
||||
- script\sql\release\1.1.1\V1.1.1_002__oa_tenant_version_alignment.sql
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## SQL 脚本注释要求
|
||||
- 表/字段 COMMENT 表达业务含义;状态类字段注明取值映射
|
||||
- 脚本头部 banner 只保留当前用途说明,不留创建/修改历程
|
||||
|
||||
### Comment Rule Group 3: project / **/controller/**/*.java
|
||||
|
||||
Applies to:
|
||||
- zhgl-modules\zhgl-demo\src\main\java\com\zhgl\demo\controller\TestBatchController.java
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## Controller 层注释要求
|
||||
- 接口注释描述业务动作与权限语义(谁能做什么),与 @SaCheckPermission 一致
|
||||
- 不写调用方式、参数示例等前端细节(那是接口文档的职责)
|
||||
|
||||
### Comment Rule Group 4: project / **/domain/**/*.java
|
||||
|
||||
Applies to:
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\bo\ContractReviewBo.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\bo\DocumentReceivingCirculateBo.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\ContractReview.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\vo\ContractReviewVo.java
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## 领域对象注释要求
|
||||
- 字段注释 = 业务含义(如"审批流当前节点编码,见 FlwNode"),不是字段名翻译("用户名→用户的名字")
|
||||
- BO 与 VO 的区别要在类注释说明用途方向
|
||||
|
||||
### Comment Rule Group 5: project / **/service/**/*.java
|
||||
|
||||
Applies to:
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractChangeServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractLedgerServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractPaymentApprovalServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractReviewServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\DocumentReceivingServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\DocumentSendServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\SealApplyServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\SuppliesItemServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\VehicleApplicationServiceImpl.java
|
||||
- zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\IVehicleApplicationService.java
|
||||
- zhgl-modules\zhgl-oa\src\test\java\com\zhgl\oa\service\impl\DocumentReceivingServiceImplTest.java
|
||||
- zhgl-modules\zhgl-system\src\main\java\com\zhgl\system\service\impl\SysSchemaHistoryServiceImpl.java
|
||||
- zhgl-modules\zhgl-workflow\src\test\java\com\zhgl\workflow\service\impl\WorkflowServiceImplTest.java
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## Service 层注释要求
|
||||
- 方法注释表达业务约束与动机(事务边界理由、状态机规则、并发前提),不复述方法名
|
||||
- 类级 Javadoc 说明领域职责与边界,一段以内
|
||||
- 复杂算法处允许行内注释解释非显然的分支条件
|
||||
|
||||
## Candidates
|
||||
|
||||
### 高危(纯过程记录,删除候选) — 5 处
|
||||
|
||||
- `script\bin\ts.sh:1-2` [悬空引用:./ry.sh 不存在] !/bin/sh ./ry.sh start 启动 stop 停止 restart 重启 status 状态
|
||||
- `script\sql\release\1.0.0\01_system.sql:269-274` [悬空引用:sys_user_data.sql 不存在] ---------------------------- 初始化-用户信息表数据 系统管理员(user_id=1) 为内置账号,保留于此;其余用户已脱敏,转移至 sys_user_data.sql ---------------------------- 保留内置管理员主键供基础关联使用,但账号默认停用且不存在可用密…
|
||||
- `script\sql\release\1.0.0\03_workflow.sql:1-3` [悬空引用:warm-flow-all.sql 不存在] ---------------------------- 0、warm-flow-all.sql ----------------------------
|
||||
- `script\sql\release\1.0.0\04_document.sql:183-188` [悬空引用:oa_document_send.sql 不存在] 设计说明(最佳实践,对照请假 test_leave / 发文 oa_document_send): 1. 审批意见不入业务表,统一由 warm-flow flow_his_task.message 记录; 2. 名称字段不入表,一律按 ID 存,由 ServiceImpl#fillNames 运行时回填到 VO; 3…
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\DocumentSendServiceImpl.java:245-251` [悬空引用:sendForm.vue 不存在] 新增发文 <p> 注意:返回 DocumentSendVo 而非 Boolean,与同模块 SealApply/DocumentReceiving 不同。 原因:前端 sendForm.vue 提交后需要获取 id 用于发起工作流,返回 VO 可避免额外查询。 </p>
|
||||
|
||||
### 中危(混合/演变叙述,拆分改写) — 33 处
|
||||
|
||||
- `.gitlab-ci.yml:79-79` [新旧对比/历史因果叙述] 单次完成测试与打包;后续 job 直接复用三个 JAR,不再重复编译。
|
||||
- `script\sql\release\1.1.0\V1.1.0_001__contract.sql:1-12` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHGL 1.1.0 数据库基线:合同管理 数据库:达梦 DM;默认模式:ZHYWGL 文件:V1.1.0_001__contract.sql;执行序号:1/4 …
|
||||
- `script\sql\release\1.1.0\V1.1.0_002__supplies.sql:1-12` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHGL 1.1.0 数据库基线:物资管理 数据库:达梦 DM;默认模式:ZHYWGL 文件:V1.1.0_002__supplies.sql;执行序号:2/4 …
|
||||
- `script\sql\release\1.1.0\V1.1.0_003__vehicle.sql:1-12` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHGL 1.1.0 数据库基线:车辆管理 数据库:达梦 DM;默认模式:ZHYWGL 文件:V1.1.0_003__vehicle.sql;执行序号:3/4 内…
|
||||
- `script\sql\release\1.1.0\V1.1.0_003__vehicle.sql:484-493` [版本号+过程信号(多为库特性说明,判别)] ============================================================ C. 菜单:车辆管理(父)+ 车辆信息/司机信息/用车申请 + 表单 + 按钮 ID 段 12400-12499:12400 父 / 12401 车辆信息 12402 司机信息 12403 用车申…
|
||||
- `script\sql\release\1.1.0\V1.1.0_004__schema_history.sql:1-12` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHGL 1.1.0 数据库基线:数据库发布历史 数据库:达梦 DM;默认模式:ZHYWGL 文件:V1.1.0_004__schema_history.sql;…
|
||||
- `script\sql\release\1.1.0\V1.1.0_005__document_seal.sql:1-13` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHGL 1.1.0 数据库迁移:公文与印章 DEL_FLAG 扩容 数据库:达梦 DM;默认模式:ZHYWGL 文件:V1.1.0_005__document_…
|
||||
- `script\sql\release\1.1.1\V1.1.1_001__document_send_version.sql:1-15` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHGL 1.1.1 数据库迁移:发文表补充 VERSION 乐观锁列 数据库:达梦 DM;默认模式:ZHYWGL 文件:V1.1.1_001__document…
|
||||
- 挂接: ALTER TABLE OA_DOCUMENT_SEND ADD (VERSION NUMBER(20) DEFAULT 0 NOT NULL);
|
||||
- `script\sql\release\1.1.1\V1.1.1_002__oa_tenant_version_alignment.sql:1-20` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHGL 1.1.1 数据库迁移:OA 基线表 TENANT_ID / VERSION 对齐 数据库:达梦 DM;默认模式:ZHYWGL 文件:V1.1.1_00…
|
||||
- `zhgl-admin\src\main\resources\application.yml:60-61` [新旧对比/历史因果叙述] 从 springboot 3.5 开始 spring自带线程池 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用
|
||||
- `zhgl-common\zhgl-common-mybatis\src\main\java\com\zhgl\common\mybatis\aspect\DataPermissionPointcut.java:21-22` [新旧对比/历史因果叙述] 优先匹配方法 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口
|
||||
- 挂接: if (method.isAnnotationPresent(DataPermission.class)) {
|
||||
- `zhgl-common\zhgl-common-social\src\main\java\me\zhyd\oauth\request\AuthDingTalkV2Request.java:21-26` [新旧对比/历史因果叙述] 新版钉钉二维码登录 @author yadong.zhang (yadong.zhang0415(a)gmail.com) @since 1.16.7
|
||||
- 挂接: public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
- `zhgl-modules\zhgl-demo\src\main\java\com\zhgl\demo\controller\TestBatchController.java:33-37` [版本号+过程信号(多为库特性说明,判别)] 新增批量方法 可完美替代 saveBatch 秒级插入上万数据 (对mysql负荷较大) <p> 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
|
||||
- `zhgl-modules\zhgl-demo\src\main\java\com\zhgl\demo\controller\TestBatchController.java:52-56` [版本号+过程信号(多为库特性说明,判别)] 新增或更新 可完美替代 saveOrUpdateBatch 高性能 <p> 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\bo\DocumentReceivingCirculateBo.java:29-29` [未定/临时过程词] 部门承办人用户 ID 列表(写入业务单并作为后续会签办理人)
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractChangeServiceImpl.java:131-131` [新旧对比/历史因果叙述] 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractChangeServiceImpl.java:361-361` [新旧对比/历史因果叙述] 提出单位已经改为字典项,后端保存并透传 dictValue,不再限定旧的中文展示文本。
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractLedgerServiceImpl.java:103-106` [新旧对比/历史因果叙述] 台账编辑只维护六月原型中的起止日期、履约情况、备注和附件。 支付列表由支付审批办结后自动写入,台账编辑不再删除或重建支付记录。
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractPaymentApprovalServiceImpl.java:112-112` [新旧对比/历史因果叙述] 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractReviewServiceImpl.java:194-194` [新旧对比/历史因果叙述] 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\ContractReviewServiceImpl.java:387-387` [新旧对比/历史因果叙述] 幂等守卫:已办结的业务不再重复执行办结副作用(防止事件重放重复生成台账)
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\DocumentReceivingServiceImpl.java:151-153` [新旧对比/历史因果叙述] 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人 发起部门过滤:收文无实体列,统一由综合管理部(102)发起。 前端编号统计传 draftDept=102 时命中全部收文;传其它值时返回空。
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\DocumentReceivingServiceImpl.java:511-511` [新旧对比/历史因果叙述] 新版收文定义据此决定合并办理节点是否包含副总,发起时固定 N,由后续节点按需调整
|
||||
- 挂接: vars.put("needDeputyManager", "N");
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\DocumentSendServiceImpl.java:118-118` [新旧对比/历史因果叙述] 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\DocumentSendServiceImpl.java:369-374` [新旧对比/历史因果叙述] 发文流程变量白名单构建:全部取自已入库的业务单据字段,网关标志统一 Y/N。 <p>会签部门负责人(signDeptLeaders)由服务端按部门配置解析, 不再信任前端回传的负责人 ID 串。</p>
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\SealApplyServiceImpl.java:124-124` [新旧对比/历史因果叙述] 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\SuppliesItemServiceImpl.java:207-210` [新旧对比/历史因果叙述] 采购入库:以当前入库确认人为操作人,新建台账批次并补一笔采购入库流水。 物品初始库存已包含本次入库数量,因此这里只写入流水,不再调用 {@code adjustStock}。
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\VehicleApplicationServiceImpl.java:185-185` [新旧对比/历史因果叙述] 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\impl\VehicleApplicationServiceImpl.java:295-302` [新旧对比/历史因果叙述] 节点3(车辆调度)完成:全量覆盖写入 dispatch 明细(先删后插)。 <p>本接口只负责保存 dispatch 明细 + 提供 schedule 查询, <b>不再写 drivers 流程变量</b>。drivers(按 sys_user.user_id 对应)由前端 在节点3 completeTask 时随 …
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\service\IVehicleApplicationService.java:67-76` [新旧对比/历史因果叙述] 节点3(车辆调度)完成:全量覆盖保存 dispatch 明细(先删后插)。 <p>本接口只保存 dispatch 明细,<b>不再写 drivers 流程变量</b>。 drivers(按 sys_user.user_id 对应)由前端在节点3 completeTask 的 taskVariables 携带。</p>…
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\support\OaWorkflowNodes.java:62-66` [新旧对比/历史因果叙述] 旧版收文定义的业务节点编码,旧实例仍在流转时返回这些编码; 与 zhgl-front 的 LEGACY_DOCUMENT_RECEIVING_NODE_CODE 一一对应。 确认不存在可继续办理的旧实例后方可移除。
|
||||
- `zhgl-modules\zhgl-oa\src\test\java\com\zhgl\oa\service\impl\DocumentReceivingServiceImplTest.java:223-223` [新旧对比/历史因果叙述] ② 旧版部门承办入口:任务校验失败无写入。
|
||||
- `zhgl-modules\zhgl-workflow\src\test\java\com\zhgl\workflow\service\impl\WorkflowServiceImplTest.java:169-171` [新旧对比/历史因果叙述] 任务节点不在集合内(如旧版节点码已收窄)→ 抛「与业务/节点不匹配」。
|
||||
|
||||
### 低危(疑似业务日期,判别后改写) — 17 处
|
||||
|
||||
- `script\sql\release\1.0.0\02_snailjob.sql:1-6` [仅日期/版本(可能是业务生效日,需判别)] SnailJob Database Transfer Tool Source Server Type : MySQL Target Server Type : Oracle Date: 2025-06-21 23:33:11
|
||||
- `script\sql\release\1.0.0\04_document.sql:1-3` [仅日期/版本(可能是业务生效日,需判别)] ZHGL 1.0.0 公文基础功能 本脚本依次初始化发文和收文。 本文件只用于新环境初始化,不得在已有业务库重复执行。
|
||||
- `script\sql\release\1.0.0\05_seal.sql:1-4` [仅日期/版本(可能是业务生效日,需判别)] ZHGL 1.0.0 印章基础功能 本脚本依次初始化印章管理和用印申请。 字典 ID: seal_type=104, seal_status=105, data=1032-1039 本文件只用于新环境初始化,不得在已有业务库重复执行。
|
||||
- `script\sql\release\1.1.0\V1.1.0_005__document_seal.sql:15-15` [仅日期/版本(可能是业务生效日,需判别)] A. 公文模块(1.0.0 原为 NUMBER(1) DEFAULT 0 NOT NULL)
|
||||
- 挂接: ALTER TABLE OA_DOCUMENT_SEND MODIFY (DEL_FLAG NUMBER(20) DEFAULT 0 NOT NULL);
|
||||
- `script\sql\release\1.1.0\V1.1.0_005__document_seal.sql:22-22` [仅日期/版本(可能是业务生效日,需判别)] B. 印章模块(1.0.0 原为 NUMBER(2) DEFAULT 0,可为 NULL)
|
||||
- 挂接: ALTER TABLE OA_SEAL_MANAGEMENT MODIFY (DEL_FLAG NUMBER(20) DEFAULT 0);
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:43-45` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月可以表示为 "2023-07"
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:68-70` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月可以表示为 "2023/07"
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:88-90` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月可以表示为 "2023.07"
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:108-110` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月可以表示为 "202307"
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:113-115` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日可以表示为 "20230722"
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:118-120` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日下午3点可以表示为 "2023072215"
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:123-125` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日下午3点30分可以表示为 "202307221530"
|
||||
- `zhgl-common\zhgl-common-core\src\main\java\com\zhgl\common\core\enums\FormatsType.java:128-130` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日下午3点30分45秒可以表示为 "20230722153045"
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\bo\ContractReviewBo.java:107-109` [仅日期/版本(可能是业务生效日,需判别)] 外文版本:1是,0否
|
||||
- 挂接: @NotBlank(message = "外文版本不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\ContractReview.java:93-95` [仅日期/版本(可能是业务生效日,需判别)] 外文版本:1是,0否
|
||||
- `zhgl-modules\zhgl-oa\src\main\java\com\zhgl\oa\domain\vo\ContractReviewVo.java:139-141` [仅日期/版本(可能是业务生效日,需判别)] 外文版本:1是,0否
|
||||
- 挂接: @ExcelProperty(value = "外文版本", converter = ExcelDictConvert.class)
|
||||
- `zhgl-modules\zhgl-system\src\main\java\com\zhgl\system\service\impl\SysSchemaHistoryServiceImpl.java:28-30` [仅日期/版本(可能是业务生效日,需判别)] 1.1.0 服务端发布白名单。登记只证明人工确认,白名单用于防止脚本名或校验值被随意伪造。
|
||||
|
||||
## Pattern Distribution
|
||||
|
||||
- old_new: 22
|
||||
- version: 18
|
||||
- date: 9
|
||||
- separator_line: 8
|
||||
- crud_verb: 7
|
||||
- dangling_ref: 5
|
||||
- version_since: 2
|
||||
- process_verb: 1
|
||||
@@ -0,0 +1,157 @@
|
||||
# Comment Review Spec (scan)
|
||||
|
||||
- repo: C:\Development\Code\zhywgl\zhywgl-front
|
||||
- rules: project (C:\Development\Code\zhywgl\zhywgl-front\.commentreview\rule.json)
|
||||
- files: 363 | comments: 3442 | candidates: 13
|
||||
- severity: P0=3 P1=9 P2=1 INFO=0
|
||||
|
||||
### Comment Rule Group 1: project / src/api/**
|
||||
|
||||
Applies to:
|
||||
- src\api\perfeval\mutualEvaluation\deptTypes.ts
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## api 层注释要求
|
||||
- 接口函数注释对应后端业务动作;类型定义靠 TS 类型自解释,不重复注释字段
|
||||
- 与后端字段含义有出入时(枚举值、单位、时区)必须注释
|
||||
|
||||
### Comment Rule Group 2: system / **
|
||||
|
||||
Applies to:
|
||||
- src\lang\en_US.ts
|
||||
- src\lang\zh_CN.ts
|
||||
- src\layout\components\Settings\index.vue
|
||||
- src\store\modules\permission.ts
|
||||
- src\types\vue-office.d.ts
|
||||
- test\common\role-baseline.ts
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
### Comment Rule Group 3: project / src/utils/**
|
||||
|
||||
Applies to:
|
||||
- src\utils\notification.ts
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## 工具函数注释要求
|
||||
- JSDoc 写清输入约束(空值、边界、格式)与副作用;纯函数的显然行为不必注释
|
||||
|
||||
### Comment Rule Group 4: project / src/views/**/*.vue
|
||||
|
||||
Applies to:
|
||||
- src\views\login.vue
|
||||
- src\views\perfeval\distribution\form.vue
|
||||
- src\views\perfeval\mutualEvaluation\personalForm.vue
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## 页面/组件注释要求
|
||||
- 组件顶部注释说明页面职责与关键交互约束(权限、状态联动),一段以内
|
||||
- 复杂表单校验规则、跨组件通信处允许行内注释解释动机
|
||||
|
||||
## Candidates
|
||||
|
||||
### 高危(纯过程记录,删除候选) — 3 处
|
||||
|
||||
- `src\store\modules\permission.ts:13-13` [悬空引用:匹配views里面所有的.vue 不存在] 匹配views里面所有的.vue文件
|
||||
- 挂接: const modules = import.meta.glob('./../../views/**/*.vue');
|
||||
- `src\types\vue-office.d.ts:3-4` [悬空引用:lib/index.js 不存在] vue-office 纯前端预览组件类型声明 pnpm 跳过了 postinstall 脚本,lib/index.js 未生成,故直接引用 v3(对应 Vue3) 构建产物
|
||||
- `test\common\role-baseline.ts:1-7` [悬空引用:test.config.json 不存在] E2E 环境角色基线。必须在 test.config.json 中显式配置 roleKeys, 不根据当前角色是否“看起来够用”放行。返回错误文案;null 表示通过。 双胞胎副本:zhywgl-e2e/test/common/role-baseline.ts(E2E health 前置使用) 与本文件内容保持一致,…
|
||||
- 挂接: export function roleBaselineError(roles: string[] | undefined, expectedRoleKeys?: string[]
|
||||
|
||||
### 中危(混合/演变叙述,拆分改写) — 9 处
|
||||
|
||||
- `src\lang\en_US.ts:4-4` [新旧对比/历史因果叙述] 兼容旧版静态路由 key(避免 meta.title 使用 dashboard/document 时无法命中翻译)
|
||||
- `src\lang\zh_CN.ts:4-4` [新旧对比/历史因果叙述] 兼容旧版静态路由 key(避免 meta.title 使用 dashboard/document 时无法命中翻译)
|
||||
- `src\layout\components\Settings\index.vue:158-158` [新旧对比/历史因果叙述] 默认锁定浅色,不再跟随操作系统的 prefers-color-scheme
|
||||
- `src\utils\notification.ts:45-45` [新旧对比/历史因果叙述] 兼容旧版浏览器,避免静默报错阻塞
|
||||
- `src\utils\notification.ts:86-86` [新旧对比/历史因果叙述] 极少数浏览器(如旧版 360)不支持 Promise 形式,降级尝试回调版
|
||||
- `src\views\login.vue:171-172` [新旧对比/历史因果叙述] 后端已关闭多租户,不再请求租户列表接口 initTenantList();
|
||||
- `src\views\perfeval\distribution\form.vue:301-301` [新旧对比/历史因果叙述] 提交前拦截:所有部门负责人行必须齐备(后端不再对负责人做阻断校验)
|
||||
- 挂接: const collectLeaderBlockers = (): string[] => {
|
||||
- `src\views\perfeval\mutualEvaluation\personalForm.vue:659-660` [新旧对比/历史因果叙述] 评分已由 approvalScore 接口写入数据库,流程变量不再携带评分(服务端任务事件不回写评分); 分管领导办理人由流程定义 SpEL #initiatorDeptId(服务端流程启动时写入)解析,不透传客户端 deptId
|
||||
- `src\views\perfeval\mutualEvaluation\personalForm.vue:731-731` [新旧对比/历史因果叙述] 查询对象由后端固定为当前登录用户及其部门,前端不再传身份参数
|
||||
|
||||
### 低危(疑似业务日期,判别后改写) — 1 处
|
||||
|
||||
- `src\api\perfeval\mutualEvaluation\deptTypes.ts:116-116` [仅日期/版本(可能是业务生效日,需判别)] 考核月份快捷搜索,格式 YYYY-MM,如 2026-08
|
||||
|
||||
## Pattern Distribution
|
||||
|
||||
- old_new: 9
|
||||
- dangling_ref: 3
|
||||
- date: 1
|
||||
@@ -0,0 +1,216 @@
|
||||
# Comment Review Spec (scan)
|
||||
|
||||
- repo: C:\Development\Code\zhywgl\zhywgl-server
|
||||
- rules: project (C:\Development\Code\zhywgl\zhywgl-server\.commentreview\rule.json)
|
||||
- files: 1119 | comments: 9581 | candidates: 42
|
||||
- severity: P0=4 P1=24 P2=14 INFO=0
|
||||
|
||||
### Comment Rule Group 1: system / **
|
||||
|
||||
Applies to:
|
||||
- script\bin\ts.sh
|
||||
- script\workflow\validate-handlers.sh
|
||||
- script\workflow\validate-releases.sh
|
||||
- zhywgl-admin\src\main\resources\application.yml
|
||||
- zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java
|
||||
- zhywgl-common\zhywgl-common-mybatis\src\main\java\com\zhywgl\common\mybatis\aspect\DataPermissionPointcut.java
|
||||
- zhywgl-common\zhywgl-common-sensitive\src\test\java\com\zhywgl\common\sensitive\core\SensitiveStrategyTest.java
|
||||
- zhywgl-common\zhywgl-common-social\src\main\java\me\zhyd\oauth\request\AuthDingTalkV2Request.java
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
### Comment Rule Group 2: project / script/sql/**
|
||||
|
||||
Applies to:
|
||||
- script\sql\release\1.0.0\01_system.sql
|
||||
- script\sql\release\1.0.0\02_workflow.sql
|
||||
- script\sql\release\1.0.0\03_seal.sql
|
||||
- script\sql\release\1.0.0\04_supplies.sql
|
||||
- script\sql\release\1.0.0\05_vehicle.sql
|
||||
- script\sql\release\1.0.0\06_vacation.sql
|
||||
- script\sql\release\1.0.0\07_schema_history.sql
|
||||
- script\sql\release\1.0.0\08_perfeval.sql
|
||||
- script\sql\release\1.0.0\09_agenda_party.sql
|
||||
- script\sql\release\1.0.0\10_attendance.sql
|
||||
- script\sql\release\1.0.0\11_duty.sql
|
||||
- script\sql\validate_sql.sh
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## SQL 脚本注释要求
|
||||
- 表/字段 COMMENT 表达业务含义;状态类字段注明取值映射
|
||||
- 脚本头部 banner 只保留当前用途说明,不留创建/修改历程
|
||||
|
||||
### Comment Rule Group 3: project / **/controller/**/*.java
|
||||
|
||||
Applies to:
|
||||
- zhywgl-modules\zhywgl-demo\src\main\java\com\zhywgl\demo\controller\TestBatchController.java
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## Controller 层注释要求
|
||||
- 接口注释描述业务动作与权限语义(谁能做什么),与 @SaCheckPermission 一致
|
||||
- 不写调用方式、参数示例等前端细节(那是接口文档的职责)
|
||||
|
||||
### Comment Rule Group 4: project / **/service/**/*.java
|
||||
|
||||
Applies to:
|
||||
- zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\AgendaPartyServiceImplTest.java
|
||||
- zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\AttendanceServiceImplTest.java
|
||||
- zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfDistributionServiceImplTest.java
|
||||
- zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfevalMutualEvaluationDeptServiceImplTest.java
|
||||
- zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfevalMutualEvaluationPersonalServiceImplTest.java
|
||||
- zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfevalSummaryReviewServiceImplTest.java
|
||||
- zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\VacationApplyServiceImplTest.java
|
||||
- zhywgl-modules\zhywgl-system\src\main\java\com\zhywgl\system\service\impl\SysSchemaHistoryServiceImpl.java
|
||||
|
||||
#### Content
|
||||
|
||||
## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动
|
||||
|
||||
## Service 层注释要求
|
||||
- 方法注释表达业务约束与动机(事务边界理由、状态机规则、并发前提),不复述方法名
|
||||
- 类级 Javadoc 说明领域职责与边界,一段以内
|
||||
- 复杂算法处允许行内注释解释非显然的分支条件
|
||||
|
||||
## Candidates
|
||||
|
||||
### 高危(纯过程记录,删除候选) — 4 处
|
||||
|
||||
- `script\bin\ts.sh:1-2` [悬空引用:./ry.sh 不存在] !/bin/sh ./ry.sh start 启动 stop 停止 restart 重启 status 状态
|
||||
- `script\sql\release\1.0.0\01_system.sql:1250-1251` [悬空引用:04_seal.sql 不存在] 业务选择器目录按钮(用户/部门/岗位,12040-12042):15 个基线角色全量绑定 (印章选择按钮 12043 的绑定随 04_seal.sql)
|
||||
- `script\sql\release\1.0.0\02_workflow.sql:1-3` [悬空引用:warm-flow-all.sql 不存在] ---------------------------- 0、warm-flow-all.sql ----------------------------
|
||||
- `script\sql\release\1.0.0\10_attendance.sql:157-161` [悬空引用:attendanceConfig.ts 不存在] ============================================================ D. 字典:attendance_mark 考勤标记 dictValue 与前端 attendanceConfig.ts 的映射注释对齐(休=3/病=5/事=6/婚=9/丧=10/ 产=11/陪=…
|
||||
|
||||
### 中危(混合/演变叙述,拆分改写) — 24 处
|
||||
|
||||
- `script\sql\release\1.0.0\04_supplies.sql:1-12` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 物品管理基线 数据库:达梦 DM;默认模式:ZHYWGL 内容:7 张业务表(物品库、库存流水、物品申领×2、物品采购×2、出库记录)、…
|
||||
- `script\sql\release\1.0.0\05_vehicle.sql:1-10` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 车辆管理基线 数据库:达梦 DM;默认模式:ZHYWGL 内容:8 张业务表、6 个业务字典及菜单/按钮权限 范围:车辆、司机、用车申请…
|
||||
- `script\sql\release\1.0.0\06_vacation.sql:1-7` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 休假管理模块 数据库:达梦 DM;默认模式:ZHYWGL 适用范围:按本目录 MANIFEST 顺序初始化全新空库 内容:休假申请表、假…
|
||||
- `script\sql\release\1.0.0\07_schema_history.sql:1-10` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 数据库发布历史基线 数据库:达梦 DM;默认模式:ZHYWGL 内容:数据库发布历史表、索引及系统管理菜单/按钮权限 范围:人工执行结果…
|
||||
- `script\sql\release\1.0.0\08_perfeval.sql:1-6` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 绩效考核基线 数据库:达梦 DM;默认模式:ZHYWGL 内容:自评互评、考核标准、汇总审核、绩效基础数据(基数/岗位系数)、绩效分配 …
|
||||
- `script\sql\release\1.0.0\08_perfeval.sql:689-698` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 绩效基础数据基线 数据库:达梦 DM;默认模式:ZHYWGL 内容:2 张业务表(绩效基础数据、绩效岗位系数)、基数初始值及菜单/按钮权…
|
||||
- `script\sql\release\1.0.0\08_perfeval.sql:821-825` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 绩效分配基线 数据库:达梦 DM;默认模式:ZHYWGL 内容:绩效分配主表、部门/人员业务明细及菜单权限 ==============…
|
||||
- `script\sql\release\1.0.0\09_agenda_party.sql:1-8` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 党委会议题呈报 数据库:达梦 DM;默认模式:ZHYWGL 适用范围:按本目录 MANIFEST 顺序初始化全新空库 内容:议题呈报表 …
|
||||
- `script\sql\release\1.0.0\10_attendance.sql:1-7` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 考勤管理模块 数据库:达梦 DM;默认模式:ZHYWGL 适用范围:按本目录 MANIFEST 顺序初始化全新空库 内容:考勤填报表、考…
|
||||
- `script\sql\release\1.0.0\11_duty.sql:1-8` [版本号+过程信号(多为库特性说明,判别)] ============================================================================= ZHYWGL 1.0.0 职务基线 数据库:达梦 DM;默认模式:ZHYWGL 内容:2 张业务表(职务、用户职务关联)及菜单/按钮权限 约束: 1. 仅用于新环…
|
||||
- `script\sql\validate_sql.sh:1-10` [新旧对比/历史因果叙述] !/usr/bin/env bash ============================================================================= 达梦数据库 (DM) SQL 基线规范与完整性校验脚本 用法: ./validate_sql.sh # 校验当前版本(根 po…
|
||||
- `zhywgl-admin\src\main\resources\application.yml:60-61` [新旧对比/历史因果叙述] 从 springboot 3.5 开始 spring自带线程池 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用
|
||||
- `zhywgl-common\zhywgl-common-mybatis\src\main\java\com\zhywgl\common\mybatis\aspect\DataPermissionPointcut.java:21-22` [新旧对比/历史因果叙述] 优先匹配方法 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口
|
||||
- 挂接: if (method.isAnnotationPresent(DataPermission.class)) {
|
||||
- `zhywgl-common\zhywgl-common-sensitive\src\test\java\com\zhywgl\common\sensitive\core\SensitiveStrategyTest.java:58-60` [新旧对比/历史因果叙述] 邮箱:本地部分被掩码,域名保留,且不再是原文。
|
||||
- `zhywgl-common\zhywgl-common-social\src\main\java\me\zhyd\oauth\request\AuthDingTalkV2Request.java:21-26` [新旧对比/历史因果叙述] 新版钉钉二维码登录 @author yadong.zhang (yadong.zhang0415(a)gmail.com) @since 1.16.7
|
||||
- 挂接: public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
- `zhywgl-modules\zhywgl-demo\src\main\java\com\zhywgl\demo\controller\TestBatchController.java:33-37` [版本号+过程信号(多为库特性说明,判别)] 新增批量方法 可完美替代 saveBatch 秒级插入上万数据 (对mysql负荷较大) <p> 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
|
||||
- `zhywgl-modules\zhywgl-demo\src\main\java\com\zhywgl\demo\controller\TestBatchController.java:52-56` [版本号+过程信号(多为库特性说明,判别)] 新增或更新 可完美替代 saveOrUpdateBatch 高性能 <p> 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
|
||||
- `zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\AgendaPartyServiceImplTest.java:48-54` [阶段/迭代编号] 党委会议题数据权限契约测试(第二阶段统一边界)。 <p>列表/详情走 Mapper scoped 查询;详情在数据范围外按「业务 ID + DWHYT 流程编码」放行流程参与人; 草稿、退回、撤销、作废仅发起人本人可见,流程参与人不豁免; 拒绝时统一文案「数据不存在或无权访问」,且不得继续查询名称、流程历史与手签章。…
|
||||
- 挂接: @ExtendWith(MockitoExtension.class)
|
||||
- `zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\AttendanceServiceImplTest.java:53-60` [阶段/迭代编号] 考勤数据权限契约测试(第二阶段统一边界)。 <p>列表/详情/导出走 Mapper scoped 查询(create_by + 业务 dept_id); 详情在数据范围外按「业务 ID + KQGL 流程编码」放行流程参与人; 草稿、退回、撤销、作废仅发起人本人可见; 详情返回流程历史与按明细人员解析的手签章投影,拒…
|
||||
- 挂接: @ExtendWith(MockitoExtension.class)
|
||||
- `zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfDistributionServiceImplTest.java:53-58` [阶段/迭代编号] 绩效分配数据权限契约测试(数据范围 ∪ 流程参与人并集,第二阶段统一边界收口)。 <p>并集分页:候选 id 全量取、scoped id 与参与人 id 求并、最终分页 ignore 二次过滤; 列表与详情统一「草稿/退回/撤销/作废仅发起人可见」;不存在与无权统一文案。</p>
|
||||
- 挂接: @ExtendWith(MockitoExtension.class)
|
||||
- `zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfevalMutualEvaluationDeptServiceImplTest.java:46-51` [阶段/迭代编号] 部门互评数据权限契约测试(第二阶段统一边界)。 <p>列表/导出走 Mapper scoped 查询;详情数据范围外按 ZPHPBM 流程参与人放行; 草稿、退回、撤销、作废仅发起人本人可见;拒绝时不得查询明细、名称与流程历史。</p>
|
||||
- 挂接: @ExtendWith(MockitoExtension.class)
|
||||
- `zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfevalMutualEvaluationPersonalServiceImplTest.java:52-58` [阶段/迭代编号] 个人自评互评数据权限契约测试(第二阶段统一边界)。 <p>列表/导出走 Mapper scoped 查询;详情数据范围外按 ZPHPGR 流程参与人放行; 草稿、退回、撤销、作废仅发起人本人可见; 审批打分必须携带任务 ID 且任务须绑定当前办理人、本业务单与部门负责人审批节点。</p>
|
||||
- 挂接: @ExtendWith(MockitoExtension.class)
|
||||
- `zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\PerfevalSummaryReviewServiceImplTest.java:48-54` [阶段/迭代编号] 汇总审核数据权限契约测试(第二阶段统一边界)。 <p>列表/导出走 Mapper scoped 查询;详情数据范围外按 HZSH 流程参与人放行; 草稿、退回、撤销、作废仅发起人本人可见; 审批模式保存以任务 ID 判定并校验归属,客户端单独声称的 nodeCode 不得跳过所有权校验。</p>
|
||||
- 挂接: @ExtendWith(MockitoExtension.class)
|
||||
- `zhywgl-modules\zhywgl-oa\src\test\java\com\zhywgl\oa\service\impl\VacationApplyServiceImplTest.java:50-56` [阶段/迭代编号] 休假数据权限契约测试(第二阶段统一边界)。 <p>列表/详情/导出走 Mapper scoped 查询(create_by + 业务 dept_id); 详情在数据范围外按「业务 ID + 三种休假流程编码」放行流程参与人(按申请类型逐码判定); 草稿、退回、撤销、作废仅发起人本人可见;详情返回流程历史与按流程办理人…
|
||||
- 挂接: @ExtendWith(MockitoExtension.class)
|
||||
|
||||
### 低危(疑似业务日期,判别后改写) — 14 处
|
||||
|
||||
- `script\sql\release\1.0.0\03_seal.sql:1-4` [仅日期/版本(可能是业务生效日,需判别)] ZHYWGL 1.0.0 印章基础功能 本脚本依次初始化印章管理和用印申请。 字典 ID: seal_type=104, seal_status=105, data=1032-1039 本文件只用于新环境初始化,不得在已有业务库重复执行。
|
||||
- `script\sql\release\1.0.0\08_perfeval.sql:341-342` [仅日期/版本(可能是业务生效日,需判别)] ZHYWGL 1.0.0 考核标准:考核标准表及菜单权限。 本文件只用于新环境初始化,不得在已有业务库重复执行。
|
||||
- `script\sql\validate_sql.sh:241-242` [仅日期/版本(可能是业务生效日,需判别)] 3.1 seed/ 目录不得存在:组织数据在 release/1.0.0/01_system.sql AA 段、 职务字典在 11_duty.sql C 段、岗位系数在 08_perfeval.sql
|
||||
- `script\workflow\validate-handlers.sh:1-7` [仅日期/版本(可能是业务生效日,需判别)] !/usr/bin/env bash 仓库级静态一致性检查(构建期,不查询任何数据库): 1. 流程 JSON 中的 post:<id> 必须在 release/1.0.0/01_system.sql AA 段(组织岗位)中存在、启用且至少绑定一名用户; 2. role:<id> 必须在 01_system.sql 角…
|
||||
- `script\workflow\validate-releases.sh:75-75` [仅日期/版本(可能是业务生效日,需判别)] 1.0.0 之外的增量版本走更严格的语义与工程契约校验
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:43-45` [仅日期/版本(可能是业务生效日,需判别)] 例例如,2023年7月可以表示为 "2023-07"
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:68-70` [仅日期/版本(可能是业务生效日,需判别)] 例例如,2023年7月可以表示为 "2023/07"
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:88-90` [仅日期/版本(可能是业务生效日,需判别)] 例例如,2023年7月可以表示为 "2023.07"
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:108-110` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月可以表示为 "202307"
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:113-115` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日可以表示为 "20230722"
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:118-120` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日下午3点可以表示为 "2023072215"
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:123-125` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日下午3点30分可以表示为 "202307221530"
|
||||
- `zhywgl-common\zhywgl-common-core\src\main\java\com\zhywgl\common\core\enums\FormatsType.java:128-130` [仅日期/版本(可能是业务生效日,需判别)] 例如,2023年7月22日下午3点30分45秒可以表示为 "20230722153045"
|
||||
- `zhywgl-modules\zhywgl-system\src\main\java\com\zhywgl\system\service\impl\SysSchemaHistoryServiceImpl.java:28-30` [仅日期/版本(可能是业务生效日,需判别)] 1.1.0 服务端发布白名单。登记只证明人工确认,白名单用于防止脚本名或校验值被随意伪造。
|
||||
|
||||
## Pattern Distribution
|
||||
|
||||
- version: 18
|
||||
- separator_line: 11
|
||||
- date: 8
|
||||
- phase_no: 7
|
||||
- crud_verb: 6
|
||||
- old_new: 5
|
||||
- dangling_ref: 4
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { runScan } from "./scan.js";
|
||||
import { renderMarkdown, renderJSON } from "./spec.js";
|
||||
import { loadRuleConfig, resolveRule } from "./rules.js";
|
||||
import { createRequire } from "node:module";
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const HELP = `comment-review (cmr) — deterministic comment-review harness (no LLM).
|
||||
|
||||
Usage:
|
||||
cmr scan [--path <dir>] [--format text|json] [--rule <file>] [--min-severity p0|p1|p2|all]
|
||||
cmr diff [--from <ref> --to <ref> | --commit <hash>] [same flags]
|
||||
cmr rules <file>... show resolved rule group per file
|
||||
cmr version
|
||||
|
||||
scan = whole-tree audit. diff = comments new/changed vs HEAD (workspace) or a ref range.`;
|
||||
|
||||
export async function runCLI(argv) {
|
||||
let [cmd, ...rest] = argv;
|
||||
if (cmd === "--help" || cmd === "-h") cmd = "help";
|
||||
if (cmd === "--version") cmd = "version";
|
||||
const opts = {};
|
||||
const files = [];
|
||||
const flags = { "--format": "format", "--rule": "rule", "--path": "path", "--from": "from", "--to": "to", "--commit": "commit", "--min-severity": "minSeverity" };
|
||||
try {
|
||||
for (let i = 0; i < rest.length; i++) {
|
||||
const arg = rest[i];
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
console.log(HELP);
|
||||
return 0;
|
||||
}
|
||||
if (arg === "--") {
|
||||
files.push(...rest.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
if (!arg.startsWith("-")) {
|
||||
files.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (!Object.hasOwn(flags, arg)) throw new Error(`unknown option: ${arg}`);
|
||||
const value = rest[++i];
|
||||
if (!value || value.startsWith("--")) throw new Error(`missing value for ${arg}`);
|
||||
opts[flags[arg]] = value;
|
||||
}
|
||||
if (opts.format && !["text", "json"].includes(opts.format)) throw new Error("--format must be text or json");
|
||||
if (opts.minSeverity) {
|
||||
opts.minSeverity = opts.minSeverity.toUpperCase();
|
||||
if (!["P0", "P1", "P2", "ALL"].includes(opts.minSeverity)) throw new Error("--min-severity must be p0, p1, p2 or all");
|
||||
}
|
||||
if (Boolean(opts.from) !== Boolean(opts.to)) throw new Error("--from and --to must be used together");
|
||||
if (opts.commit && opts.from) throw new Error("--commit cannot be combined with --from/--to");
|
||||
if (cmd !== "diff" && (opts.from || opts.to || opts.commit)) throw new Error("revision options require diff");
|
||||
if (cmd === "rules" && !files.length) throw new Error("rules requires at least one file");
|
||||
if (cmd !== "rules" && files.length) throw new Error(`unexpected argument: ${files[0]}`);
|
||||
if (cmd === "rules" && (opts.path || opts.minSeverity)) throw new Error("rules supports --rule and --format only");
|
||||
} catch (err) {
|
||||
console.error(`${err.message}\n\n${HELP}`);
|
||||
return 2;
|
||||
}
|
||||
const repoDir = process.cwd();
|
||||
|
||||
if (cmd === "version") {
|
||||
console.log("comment-review " + require("../package.json").version);
|
||||
return 0;
|
||||
}
|
||||
if (cmd === "help" || !cmd) {
|
||||
console.log(HELP);
|
||||
return 0;
|
||||
}
|
||||
if (cmd === "rules") {
|
||||
const config = loadRuleConfig(opts.rule, repoDir);
|
||||
const resolved = files.map((file) => ({ file, ...resolveRule(config, file) }));
|
||||
if (opts.format === "json") console.log(JSON.stringify(resolved, null, 2));
|
||||
else for (const r of resolved) {
|
||||
console.log(`${r.file}\n -> ${r.source} / ${r.pattern}\n${r.text}`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (cmd !== "scan" && cmd !== "diff") {
|
||||
console.error(`unknown command: ${cmd}\n\n${HELP}`);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const spec = runScan(repoDir, { ...opts, mode: cmd });
|
||||
console.log(opts.format === "json" ? renderJSON(spec) : renderMarkdown(spec));
|
||||
return 0;
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Lightweight lexical extraction; quoted data is skipped before comment detection.
|
||||
|
||||
const DECL_RE =
|
||||
/^\s*(?:@[\w.]+\s+)*(?:public|private|protected|static|final|abstract|synchronized|default|async|export|declare|extern)?[^\n]*?\b(class|interface|enum|record|def|fun|func|function|const|let|var|void|[A-Z]\w*\s+\w+\s*\()|^\s*<(?:template|script|el-|a-|v-)/;
|
||||
|
||||
const LANGS = {
|
||||
".java": { line: ["//"], block: [["/*", "*/"]], doc: "/**" },
|
||||
".ts": { line: ["//"], block: [["/*", "*/"]], doc: "/**" },
|
||||
".tsx": { line: ["//"], block: [["/*", "*/"]], doc: "/**" },
|
||||
".js": { line: ["//"], block: [["/*", "*/"]], doc: "/**" },
|
||||
".jsx": { line: ["//"], block: [["/*", "*/"]], doc: "/**" },
|
||||
".mjs": { line: ["//"], block: [["/*", "*/"]], doc: "/**" },
|
||||
".cjs": { line: ["//"], block: [["/*", "*/"]], doc: "/**" },
|
||||
".vue": { line: ["//"], block: [["/*", "*/"], ["<!--", "-->"]], doc: "/**" },
|
||||
".xml": { line: [], block: [["<!--", "-->"]] },
|
||||
".html": { line: [], block: [["<!--", "-->"]] },
|
||||
".sql": { line: ["--"], block: [["/*", "*/"]] },
|
||||
".properties": { line: ["#", "!"] },
|
||||
".yml": { line: ["#"] },
|
||||
".yaml": { line: ["#"] },
|
||||
".sh": { line: ["#"] },
|
||||
".py": { line: ["#"], block: [['"""', '"""'], ["'''", "'''"]] },
|
||||
};
|
||||
|
||||
export function langFor(file) {
|
||||
const lower = file.toLowerCase();
|
||||
for (const ext of Object.keys(LANGS)) {
|
||||
if (lower.endsWith(ext)) return LANGS[ext];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractComments(text, file) {
|
||||
const lang = langFor(file);
|
||||
if (!lang) return [];
|
||||
const isPython = /\.py$/i.test(file);
|
||||
const isJava = /\.java$/i.test(file);
|
||||
const isProperties = /\.properties$/i.test(file);
|
||||
const isYaml = /\.ya?ml$/i.test(file);
|
||||
const isShell = /\.sh$/i.test(file);
|
||||
const lines = text.split(/\r?\n/);
|
||||
const comments = [];
|
||||
let block = null; // { opener, closer, start, buf }
|
||||
let lineCmt = null; // { start, buf }
|
||||
let inString = null; // { quote, multiline }
|
||||
|
||||
const flushLine = (endIdx) => {
|
||||
if (!lineCmt) return;
|
||||
pushComment(lineCmt.start, endIdx, lineCmt.buf.join("\n"), "line");
|
||||
lineCmt = null;
|
||||
};
|
||||
const pushComment = (start, end, body, kind) => {
|
||||
const cleaned =
|
||||
kind === "block" || kind === "doc"
|
||||
? body
|
||||
.split("\n")
|
||||
.map((l) => l.replace(/^\s*\*:? ?/, ""))
|
||||
.join("\n")
|
||||
: body;
|
||||
comments.push({
|
||||
file,
|
||||
start: start + 1,
|
||||
end: end + 1,
|
||||
kind,
|
||||
text: cleaned,
|
||||
symbol: findSymbol(lines, end),
|
||||
});
|
||||
};
|
||||
const findSymbol = (lines, endIdx) => {
|
||||
for (let i = endIdx + 1; i < Math.min(endIdx + 4, lines.length); i++) {
|
||||
const l = lines[i];
|
||||
if (!l.trim()) continue;
|
||||
if (l.trim().startsWith("//") || l.trim().startsWith("*")) continue;
|
||||
const m = l.match(DECL_RE);
|
||||
return m ? l.trim().slice(0, 100) : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const raw = lines[i];
|
||||
let idx = 0;
|
||||
if (inString && !inString.multiline) inString = null;
|
||||
|
||||
if (block) {
|
||||
const closePos = raw.indexOf(block.closer);
|
||||
if (closePos >= 0) {
|
||||
block.buf.push(raw.slice(0, closePos));
|
||||
pushComment(block.start, i, block.buf.join("\n"), block.doc ? "doc" : "block");
|
||||
idx = closePos + block.closer.length;
|
||||
block = null;
|
||||
} else {
|
||||
block.buf.push(raw);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (; idx < raw.length; ) {
|
||||
const rest = raw.slice(idx);
|
||||
if (inString) {
|
||||
if (raw[idx] === "\\") {
|
||||
idx += 2;
|
||||
} else if (rest.startsWith(inString.quote)) {
|
||||
idx += inString.quote.length;
|
||||
inString = null;
|
||||
} else idx++;
|
||||
continue;
|
||||
}
|
||||
const ch = raw[idx];
|
||||
const triple = (isPython || isJava) && (rest.startsWith('"""') || (isPython && rest.startsWith("'''")));
|
||||
const pythonDoc = isPython && triple && !raw.slice(0, idx).trim();
|
||||
if (triple && !pythonDoc) {
|
||||
inString = { quote: rest.slice(0, 3), multiline: true };
|
||||
idx += 3;
|
||||
continue;
|
||||
}
|
||||
if (!pythonDoc && (ch === '"' || ch === "'" || ch === "`")) {
|
||||
inString = { quote: ch, multiline: ch === "`" || isShell || isYaml };
|
||||
idx++;
|
||||
continue;
|
||||
}
|
||||
const opener = (lang.block ?? []).find(([o]) => rest.startsWith(o));
|
||||
if (opener) {
|
||||
const doc = pythonDoc || Boolean(lang.doc && rest.startsWith(lang.doc));
|
||||
const bodyStart = idx + opener[0].length + (doc && opener[0] === "/*" && !rest.startsWith("/**/") ? 1 : 0);
|
||||
const closePos = raw.indexOf(opener[1], idx + opener[0].length);
|
||||
if (closePos >= 0) {
|
||||
pushComment(i, i, raw.slice(bodyStart, closePos), doc ? "doc" : "block");
|
||||
idx = closePos + opener[1].length;
|
||||
} else {
|
||||
block = {
|
||||
opener: opener[0],
|
||||
closer: opener[1],
|
||||
start: i,
|
||||
doc,
|
||||
buf: [raw.slice(bodyStart)],
|
||||
};
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const lineOpener = lang.line.find((o) => rest.startsWith(o) &&
|
||||
(!isProperties || !raw.slice(0, idx).trim()) &&
|
||||
(!(isYaml || isShell) || idx === 0 || /\s/.test(raw[idx - 1])));
|
||||
if (lineOpener !== undefined && lineOpener !== "") {
|
||||
// "//" preceded by ":" is a URL scheme, not a comment
|
||||
if (lineOpener === "//" && idx > 0 && raw[idx - 1] === ":") {
|
||||
idx++;
|
||||
continue;
|
||||
}
|
||||
const marker = lineOpener;
|
||||
lineCmt = { start: i, buf: [rest.slice(marker.length)] };
|
||||
// consecutive line comments merge into one unit
|
||||
let j = i + 1;
|
||||
while (j < lines.length && lines[j].trim().startsWith(marker)) {
|
||||
lineCmt.buf.push(lines[j].trim().slice(marker.length));
|
||||
j++;
|
||||
}
|
||||
flushLine(j - 1);
|
||||
i = j - 1;
|
||||
idx = raw.length;
|
||||
continue;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
if (block) pushComment(block.start, lines.length - 1, block.buf.join("\n"), block.doc ? "doc" : "block");
|
||||
return comments;
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
function git(repoDir, ...args) {
|
||||
return execFileSync("git", ["-C", repoDir, ...args], {
|
||||
encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
export function repositoryRoot(dir) {
|
||||
return git(dir, "rev-parse", "--show-toplevel").trim();
|
||||
}
|
||||
|
||||
function revision(repoDir, ref) {
|
||||
return git(repoDir, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`).trim();
|
||||
}
|
||||
|
||||
export function treeFiles(repoDir, ref) {
|
||||
// Only regular blobs can be scanned. Symlinks and submodules are not source files.
|
||||
return git(repoDir, "ls-tree", "-rz", "--full-tree", ref).split("\0").filter(Boolean)
|
||||
.filter((entry) => /^100(?:644|755) blob /.test(entry))
|
||||
.map((entry) => entry.slice(entry.indexOf("\t") + 1));
|
||||
}
|
||||
|
||||
export function readRevisionFile(repoDir, ref, path) {
|
||||
return git(repoDir, "show", `${ref}:${path}`);
|
||||
}
|
||||
|
||||
export function diffContext(repoDir, opts) {
|
||||
if (Boolean(opts.from) !== Boolean(opts.to)) throw new Error("--from and --to must be used together");
|
||||
if (opts.commit && opts.from) throw new Error("--commit cannot be combined with --from/--to");
|
||||
let base = null;
|
||||
let target = null;
|
||||
if (opts.commit) {
|
||||
target = revision(repoDir, opts.commit);
|
||||
// A merge commit is compared with its first parent, like a normal change review.
|
||||
base = git(repoDir, "rev-list", "--parents", "-n", "1", target).trim().split(/\s+/)[1] ?? null;
|
||||
} else if (opts.from) {
|
||||
target = revision(repoDir, opts.to);
|
||||
base = git(repoDir, "merge-base", revision(repoDir, opts.from), target).trim();
|
||||
} else {
|
||||
// A repository before its first commit has an empty baseline.
|
||||
const head = git(repoDir, "rev-parse", "--revs-only", "HEAD").trim();
|
||||
if (head) base = revision(repoDir, "HEAD");
|
||||
}
|
||||
const targetFiles = target ? treeFiles(repoDir, target) : null;
|
||||
let files;
|
||||
if (target && !base) files = targetFiles;
|
||||
else if (base) {
|
||||
const refs = target ? [base, target] : [base];
|
||||
files = git(repoDir, "diff", "--no-ext-diff", "--no-renames", "--name-only", "--diff-filter=ACMT", "-z", ...refs, "--").split("\0").filter(Boolean);
|
||||
} else files = git(repoDir, "ls-files", "-z", "--cached").split("\0").filter(Boolean);
|
||||
if (!target) files.push(...git(repoDir, "ls-files", "-z", "--others", "--exclude-standard").split("\0").filter(Boolean));
|
||||
return { base, target, targetFiles, baseFiles: new Set(base ? treeFiles(repoDir, base) : []), files: [...new Set(files)].sort() };
|
||||
}
|
||||
|
||||
export function newComments(comments, previous) {
|
||||
// Counts preserve newly added copies of an existing comment. A Set loses them.
|
||||
const counts = new Map();
|
||||
for (const c of previous) counts.set(c.text.trim(), (counts.get(c.text.trim()) ?? 0) + 1);
|
||||
return comments.filter((c) => {
|
||||
const key = c.text.trim();
|
||||
const count = counts.get(key) ?? 0;
|
||||
if (!count) return true;
|
||||
counts.set(key, count - 1);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// doublestar-style matching (subset): **, *, ?; case-insensitive, like ocr lowercases paths.
|
||||
export function globToRegExp(pattern) {
|
||||
let re = "";
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
const c = pattern[i];
|
||||
if (c === "*") {
|
||||
if (pattern[i + 1] === "*") {
|
||||
// "**/" matches zero or more path segments; bare "**" matches anything
|
||||
if (pattern[i + 2] === "/") {
|
||||
re += "(?:.*/)?";
|
||||
i += 2;
|
||||
} else {
|
||||
re += ".*";
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
re += "[^/]*";
|
||||
}
|
||||
} else if (c === "?") {
|
||||
re += "[^/]";
|
||||
} else if ("\\^$.|+()[]{}".includes(c)) {
|
||||
re += "\\" + c;
|
||||
} else {
|
||||
re += c;
|
||||
}
|
||||
}
|
||||
return new RegExp("^" + re + "$", "i");
|
||||
}
|
||||
|
||||
export function matchGlob(pattern, path) {
|
||||
return globToRegExp(pattern).test(path.replaceAll("\\", "/"));
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
// Process-narration pattern lexicon. Candidates are graded, not condemned:
|
||||
// the host agent makes the final process-vs-constraint call per candidate.
|
||||
|
||||
export const PATTERNS = [
|
||||
{
|
||||
id: "date",
|
||||
weight: "weak",
|
||||
re: /(?<![\d.])(19|20)\d{2}[-/年.]\s?\d{1,2}(?:[-/月.]\s?\d{1,2}(?:日|号)?)?(?!\d)|(?<!\d)\d{1,2}月\d{1,2}[日号](?!\d)|(?<!\d)(19|20)\d{6}(?!\d)/g,
|
||||
label: "日期标记",
|
||||
},
|
||||
{
|
||||
id: "version",
|
||||
weight: "weak",
|
||||
re: /\bv\s*\d+(?:\.\d+)+\b|(?<![\d.])\d+\.\d+\.\d+\b(?![.\d])(?!\s*[;)\]])|版本\s*[::]?\s*\d+(?:\.\d+)*/g,
|
||||
label: "版本号",
|
||||
},
|
||||
{
|
||||
id: "strong_process",
|
||||
weight: "strong",
|
||||
re: /定稿|返工|推翻|废除|废弃|暂缓上线|回退决定|取消上线/g,
|
||||
label: "过程动词(强)",
|
||||
},
|
||||
{
|
||||
id: "process_verb",
|
||||
weight: "medium",
|
||||
re: /待定|待确认|暂定|后续(?!会签)(会|再|将)|先(这样|保留|按此)|临时(方案|处理|改为|先用)|暂时(够用|先|按)/g,
|
||||
label: "未定/临时过程词",
|
||||
},
|
||||
{
|
||||
id: "crud_verb",
|
||||
weight: "info",
|
||||
re: /(新增|删除|修改|更新|变更|调整|恢复|取消|增加)了?于?/g,
|
||||
label: "CRUD 动词(仅作组合证据)",
|
||||
},
|
||||
{
|
||||
id: "attribution",
|
||||
weight: "strong",
|
||||
re: /[\u4e00-\u9fa5]{1,3}(总|经理|老师|工)(?=[::,,\s]|要求|说|指示)|甲方|客户(方|要求|说)|领导(要求|说|指示)|项目组(要求|说)|产品(要求|说)/g,
|
||||
label: "人物/组织归因",
|
||||
},
|
||||
{
|
||||
id: "old_new",
|
||||
weight: "medium",
|
||||
re: /原来|原先|此前|之前是|最初|旧版|新版|老版本|后(改|换|调)为|不再[\u4e00-\u9fa5]{1,4}|现已(改|换|迁)|改为[::]|历史上|曾致|曾被|早期版本|迁移期|取代原先|反哺/g,
|
||||
label: "新旧对比/历史因果叙述",
|
||||
},
|
||||
{
|
||||
id: "measured_recall",
|
||||
weight: "medium",
|
||||
re: /实测|实测案例|测得|挂死过|卡死过|曾在.*环境|旧环境|原环境/g,
|
||||
label: "实测/环境回忆",
|
||||
},
|
||||
{
|
||||
// "v2.1 起"/"版本 v2 起" narrate the code's own evolution; "发布定义 v6 起"
|
||||
// anchors an external object's version and is exempted by context filter below.
|
||||
id: "version_since",
|
||||
weight: "medium",
|
||||
re: /\bv\d+(?:\.\d+)*\s*起|\d+(?:\.\d+)+\s*起/g,
|
||||
label: "版本演变起点(vN 起)",
|
||||
},
|
||||
{
|
||||
id: "phase_no",
|
||||
weight: "medium",
|
||||
re: /第[一二三四五六七八九十]+阶段|阶段\s*[0-9一二三四五六七八九十]+(?:\s*[((][^))]{0,12}[))])?|\bPhase\s*-?\d+\b/g,
|
||||
label: "阶段/迭代编号",
|
||||
},
|
||||
{
|
||||
id: "commit_ref",
|
||||
weight: "medium",
|
||||
re: /(?:commit|提交)\s*[::]?\s*[0-9a-f]{7,40}\b|自\s*[0-9a-f]{7,40}\s*(?:适配|并入|迁|同步|移植|引入)/gi,
|
||||
label: "提交溯源",
|
||||
},
|
||||
{
|
||||
id: "review_no",
|
||||
weight: "medium",
|
||||
re: /\bR\d{1,3}\b\s*(修正|意见|建议|备注)|评审意见\s*[Rr]?\d+/g,
|
||||
label: "评审编号引用",
|
||||
},
|
||||
{
|
||||
// heading only when standing on its own line — "合同变更记录" (a business
|
||||
// record type) must not trip this
|
||||
id: "revision_heading",
|
||||
weight: "strong",
|
||||
re: /^[ \t]*(?:\*+|-+)?[ \t]*(?:修订记录|变更记录|修改历史|update\s+history|changelog)[ \t]*[::]?[ \t]*$/gim,
|
||||
label: "修订记录标题",
|
||||
},
|
||||
{
|
||||
id: "separator_line",
|
||||
weight: "medium",
|
||||
re: /^\s*[-=*_]{4,}\s*$/gm,
|
||||
label: "分隔线(需与其他过程信号共现)",
|
||||
},
|
||||
{
|
||||
id: "scheme_debate",
|
||||
weight: "medium",
|
||||
re: /方案[一二三ABCap\d]{0,3}[::((]|第一?[版次].*?(改|换|弃)|重构前|重构后/g,
|
||||
label: "方案演变叙述",
|
||||
},
|
||||
{
|
||||
id: "todo",
|
||||
weight: "info",
|
||||
re: /\b(TODO|FIXME|HACK|XXX)\b[::\s]?(\d{4}[-/]\d{1,2}[-/]\d{1,2})?/g,
|
||||
label: "TODO/FIXME 台账",
|
||||
},
|
||||
];
|
||||
|
||||
export function classifyComment(comment, { ignoredReferences = [] } = {}) {
|
||||
// @-tag lines (@version/@since/@author/@param) are standard javadoc, not prose;
|
||||
// quoted spans are usually example code (SQL strings), not narration.
|
||||
let prose = comment.text;
|
||||
// Exempt the resolved filename only; adjacent change history still needs review.
|
||||
for (const ref of ignoredReferences) prose = prose.split(ref).join(" ");
|
||||
prose = prose
|
||||
.split("\n")
|
||||
.filter((l) => !/^\s*@/.test(l))
|
||||
.join("\n")
|
||||
.replace(/'[^'\n]*'|"[^"\n]*"/g, '""');
|
||||
const hits = [];
|
||||
for (const p of PATTERNS) {
|
||||
const matches = [...prose.matchAll(p.re)];
|
||||
if (!matches.length) continue;
|
||||
let kept = matches.map((m) => ({ text: m[0], index: m.index }));
|
||||
// date: example values in format documentation ("例如 2023年7月…", "格式
|
||||
// YYYY-MM,如 2026-08") are teaching material, not change stamps.
|
||||
if (p.id === "date") {
|
||||
kept = kept.filter((m) => !/例如|示例|样例|可以表示为|格式[::]?\s*$|如[::,,]?\s*$/.test(prose.slice(Math.max(0, m.index - 8), m.index)));
|
||||
}
|
||||
// version: "外文版本:1是" documents a field's enumeration values (版本 is
|
||||
// the tail of a longer business word), and "3.5.0 版本 增加 xxx" quotes a
|
||||
// library capability — neither is this code's version history.
|
||||
if (p.id === "version") {
|
||||
kept = kept.filter((m) => {
|
||||
if (/[\u4e00-\u9fa5]$/.test(prose.slice(Math.max(0, m.index - 1), m.index))) return false;
|
||||
const after = prose.slice(m.index + m.text.length, m.index + m.text.length + 12);
|
||||
return !/版本?\s*(?:增加|新增|支持|引入|提供)\s*[A-Za-z_]/.test(after);
|
||||
});
|
||||
}
|
||||
// attribution: 副总(经理)/汇总/手工/人工 are role or compound words, never
|
||||
// a person being addressed — no Chinese surname contains 副/汇/手/人.
|
||||
if (p.id === "attribution") {
|
||||
kept = kept.filter((m) => !/[副汇手人]/.test(m.text));
|
||||
}
|
||||
// version_since: "v2.1 起"/"版本 v2 起" narrate the code's own evolution; "发布定义 v6 起"
|
||||
// anchors an external object's version and is exempted by context filter below.
|
||||
if (p.id === "version_since") {
|
||||
kept = kept.filter((m) => !/定义|库|依赖|引擎|框架/.test(prose.slice(Math.max(0, m.index - 12), m.index)));
|
||||
}
|
||||
// old_new: "兼容旧版 X" states a live compatibility requirement; 旧版/新版
|
||||
// anchored to business objects (workflow instances, definitions, routes,
|
||||
// browsers, app versions) are versioned facts, not history narration.
|
||||
if (p.id === "old_new") {
|
||||
kept = kept.filter((m) => {
|
||||
if (/兼容[的]?$/.test(prose.slice(Math.max(0, m.index - 6), m.index))) return false;
|
||||
if (!/^(?:旧版|新版|老版本)/.test(m.text)) return true;
|
||||
const after = prose.slice(m.index, m.index + m.text.length + 12);
|
||||
return !/实例|单据|记录|数据|仍在|并存|流转|运行|处理中|定义|节点|入口|路由|据此|浏览器|钉钉|微信|客户端|小程序|不支持/.test(after);
|
||||
});
|
||||
}
|
||||
if (kept.length) hits.push({ id: p.id, weight: p.weight, label: p.label, samples: [...new Set(kept.map((m) => m.text))].slice(0, 4) });
|
||||
}
|
||||
if (!hits.length) return null;
|
||||
if (hits.every((h) => h.id === "separator_line" || h.id === "crud_verb")) return null;
|
||||
const active = hits;
|
||||
const has = (id) => active.some((h) => h.id === id);
|
||||
const strong = active.some((h) => h.weight === "strong" && h.id !== "attribution");
|
||||
const attribution = has("attribution");
|
||||
const dateAnchor = has("date");
|
||||
const anchor = dateAnchor || has("version");
|
||||
const mediumSignals = active.filter((h) => h.weight === "medium").length;
|
||||
const heavyHits = active.filter((h) => h.weight === "strong" || h.weight === "medium");
|
||||
const processEvidence = attribution || has("old_new") || has("process_verb") || has("crud_verb");
|
||||
|
||||
let severity, reason;
|
||||
if (has("todo") && heavyHits.length === 0) {
|
||||
severity = "INFO";
|
||||
reason = "TODO 台账(日期视为截止/追溯信息,随台账记录)";
|
||||
} else if (has("revision_heading") || strong || (dateAnchor && processEvidence)) {
|
||||
severity = "P0";
|
||||
reason = strong && !has("revision_heading") ? "强过程动词" : has("revision_heading") ? "修订记录标题" : "日期+过程信号组合";
|
||||
} else if ((anchor && processEvidence) || mediumSignals >= 1 || attribution || has("scheme_debate")) {
|
||||
severity = "P1";
|
||||
reason = attribution ? "人物归因" : has("scheme_debate") ? "方案演变" : anchor ? "版本号+过程信号(多为库特性说明,判别)" : active.find((h) => h.weight === "medium")?.label ?? "弱过程信号";
|
||||
} else if (anchor) {
|
||||
severity = "P2";
|
||||
reason = "仅日期/版本(可能是业务生效日,需判别)";
|
||||
} else {
|
||||
severity = "INFO";
|
||||
reason = "TODO 台账";
|
||||
}
|
||||
|
||||
return { ...comment, hits, severity, reason };
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Layered rule resolution, mirroring ocr: CLI --rule > project .commentreview/rule.json
|
||||
// > user ~/.commentreview/rule.json > built-in default. First matching glob wins.
|
||||
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, dirname, resolve } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { matchGlob } from "./glob.js";
|
||||
|
||||
export const DEFAULT_RULE_TEXT = `## 注释通用质量(内置规则)
|
||||
|
||||
注释只回答三问:**是什么**(作用域/职责)、**受何约束**(业务规则、前提、坑)、**行为是什么**(非显然的行为说明)。
|
||||
|
||||
### 禁止(过程叙述,git 提交消息的职责)
|
||||
- 日期戳、迭代/阶段编号(阶段 3、第二阶段、Phase N)、文档版本号
|
||||
- 版本演变起点("v2.1 起…")→ 改写为当前结构语义
|
||||
- 实测/性能回忆("实测 594s 挂死")、"曾在某环境"类环境记忆
|
||||
- 提交溯源(commit hash)、来源仓、评审编号引用(R6 修正)
|
||||
- 历史因果词(曾致/曾被/早期版本误用/迁移期教训/取代原先/反哺)
|
||||
- 悬空引用:引用仓库中不存在的文档/文件/章节 → 删除引用
|
||||
|
||||
### 豁免(合法保留,勿误杀)
|
||||
- 引用**真实存在**的带日期文档,按实际文件名保留(改名会伪造记录)
|
||||
- 数据/行为事实:动画时长、menu_id、数据版本字段、SQL 日期字面量
|
||||
- 环境频率现状描述("偶发超过 15s"支撑超时取值)= 现状约束
|
||||
- vendor/第三方文件注释不动`;
|
||||
|
||||
export function loadRuleConfig(explicitPath, repoDir) {
|
||||
const candidates = [];
|
||||
if (explicitPath) {
|
||||
const path = resolve(repoDir, explicitPath);
|
||||
if (!existsSync(path)) throw new Error(`Rule file does not exist: ${path}`);
|
||||
candidates.push({ path, source: "cli" });
|
||||
} else {
|
||||
// Search ancestors so commands run inside a source folder inherit project rules.
|
||||
let dir = resolve(repoDir);
|
||||
while (true) {
|
||||
candidates.push({ path: join(dir, ".commentreview", "rule.json"), source: "project" });
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
candidates.push({ path: join(homedir(), ".commentreview", "rule.json"), source: "user" });
|
||||
}
|
||||
for (const c of candidates) {
|
||||
if (existsSync(c.path)) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(readFileSync(c.path, "utf8"));
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("expected an object");
|
||||
if (parsed.rules !== undefined && !Array.isArray(parsed.rules)) throw new Error("rules must be an array");
|
||||
if (parsed.excludes !== undefined && (!Array.isArray(parsed.excludes) || parsed.excludes.some((p) => typeof p !== "string" || !p))) throw new Error("excludes must contain glob strings");
|
||||
for (const r of parsed.rules ?? []) {
|
||||
if (!r || typeof r.path !== "string" || !r.path || typeof r.rule !== "string") throw new Error("each rule requires a path glob and rule text");
|
||||
if (r.merge_system_rule !== undefined && typeof r.merge_system_rule !== "boolean") throw new Error("merge_system_rule must be boolean");
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Invalid rule file ${c.path}: ${err.message}`, { cause: err });
|
||||
}
|
||||
return { source: c.source, path: c.path, rules: parsed.rules ?? [], excludes: parsed.excludes ?? [] };
|
||||
}
|
||||
}
|
||||
return { source: "system", path: null, rules: [], excludes: [] };
|
||||
}
|
||||
|
||||
export function resolveRule(config, relPath) {
|
||||
const norm = relPath.replaceAll("\\", "/");
|
||||
for (const r of config.rules) {
|
||||
if (matchGlob(r.path, norm)) {
|
||||
return {
|
||||
source: config.source,
|
||||
pattern: r.path,
|
||||
text: r.merge_system_rule === false ? r.rule : DEFAULT_RULE_TEXT + "\n\n" + r.rule,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { source: "system", pattern: "**", text: DEFAULT_RULE_TEXT };
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
import { readdirSync, readFileSync, lstatSync } from "node:fs";
|
||||
import { join, relative, resolve, posix, isAbsolute } from "node:path";
|
||||
import { extractComments, langFor } from "./extract.js";
|
||||
import { classifyComment } from "./patterns.js";
|
||||
import { loadRuleConfig, resolveRule } from "./rules.js";
|
||||
import { globToRegExp } from "./glob.js";
|
||||
import { repositoryRoot, diffContext, readRevisionFile, newComments } from "./git.js";
|
||||
|
||||
const SKIP_DIRS = new Set([".git", "node_modules", "target", "dist", ".codegraph", ".idea", ".commentreview", ".opencodereview", "build", "out", "tmp", "ops"]);
|
||||
// Directories whose contents are never in the inventory (skipped by the walk or
|
||||
// typical build outputs): absence there proves nothing, so refs into them are
|
||||
// only reported for verification.
|
||||
const UNINDEXED_DIRS = new Set([...SKIP_DIRS, "lib", "libs", "coverage", "logs"]);
|
||||
const REF_RE = /[A-Za-z0-9_\-\u4e00-\u9fa5./\\]{3,}\.(?:md|sql|json|ts|tsx|js|vue|java|xml|yml|yaml|sh|properties)\b/gi;
|
||||
const DATED_NAME_RE = /(^|[^0-9])(19|20)\d{2}[^0-9]|(?:^|[^0-9])\d{1,2}-\d{1,2}(?:[^0-9]|$)/;
|
||||
// usage-line placeholders ("./ry.sh start" style docs use file.sql as an argument slot)
|
||||
const PLACEHOLDER_STEM_RE = /^(?:file|files|xxx|name|input|output|example|your[-_]?[a-z]*|demo)$/i;
|
||||
|
||||
export function walkFiles(root, dir = root, acc = []) {
|
||||
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
||||
for (const entry of entries) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isSymbolicLink()) continue;
|
||||
if (entry.isDirectory()) {
|
||||
if (!SKIP_DIRS.has(entry.name)) walkFiles(root, full, acc);
|
||||
} else if (entry.isFile()) acc.push(full);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Reference targets must resolve against the whole repo, so the index is
|
||||
// always built from the repo root regardless of --path.
|
||||
function buildFileIndex(files) {
|
||||
const paths = new Set();
|
||||
const basenames = new Set();
|
||||
const dirs = new Set();
|
||||
for (const rel of files) {
|
||||
paths.add(rel);
|
||||
basenames.add(rel.split("/").pop());
|
||||
for (let i = rel.indexOf("/"); i > 0; i = rel.indexOf("/", i + 1)) dirs.add(rel.slice(0, i));
|
||||
}
|
||||
return { paths, basenames, dirs };
|
||||
}
|
||||
|
||||
// Near hits that prove the file moved or was renamed: same basename elsewhere,
|
||||
// same stem with another extension, or a stem differing only in digits. The
|
||||
// commenting file itself never counts — a twin copy under another path is a
|
||||
// legitimate cross-repo reference, not moving evidence.
|
||||
function nearMisses(rel, base, self, index) {
|
||||
const found = new Set();
|
||||
const stem = base.replace(/\.[^.]+$/, "");
|
||||
const ext = base.slice(stem.length);
|
||||
const fuzzy = (s) => s.replace(/\d+/g, "#");
|
||||
const dir = rel.includes("/") ? rel.slice(0, rel.length - base.length) : null;
|
||||
for (const p of index.paths) {
|
||||
if (p === self) continue;
|
||||
const pBase = p.slice(p.lastIndexOf("/") + 1);
|
||||
if (pBase === base) {
|
||||
if (p !== rel) found.add(p);
|
||||
continue;
|
||||
}
|
||||
if (!dir || !p.startsWith(dir)) continue;
|
||||
const pStem = pBase.replace(/\.[^.]+$/, "");
|
||||
const pExt = pBase.slice(pStem.length);
|
||||
if (pStem === stem && pExt !== ext) found.add(p);
|
||||
else if (pExt === ext && pStem !== stem && fuzzy(pStem) === fuzzy(stem)) found.add(p);
|
||||
}
|
||||
return [...found].sort().slice(0, 3);
|
||||
}
|
||||
|
||||
// An unresolved ref is only "confirmed broken" when the repository itself
|
||||
// supplies evidence (moved file, or the target directory exists without the
|
||||
// file). External paths, build outputs and unverifiable bare names go back to
|
||||
// the agent for verification, with what was searched and what came close.
|
||||
function gradeDangling(ref, comment, index) {
|
||||
const norm = ref.replaceAll("\\", "/");
|
||||
const rel = norm.replace(/^\.\//, "");
|
||||
const base = rel.split("/").pop();
|
||||
const rootRel = posix.normalize(rel);
|
||||
const srcRel = posix.normalize(posix.join(posix.dirname(comment.file), rel));
|
||||
// Bare names additionally resolve against every basename in the repo.
|
||||
const searched = [...new Set(rel.includes("/") ? [rootRel, srcRel] : [rootRel, srcRel, "全库同名"])];
|
||||
const evidence = { ref, searched, alternatives: [] };
|
||||
|
||||
if (/[\u4e00-\u9fa5]/.test(base) && /[A-Za-z]/.test(base)) {
|
||||
return { ...evidence, tier: "verify", note: "引用混有中文描述,可能不是文件路径" };
|
||||
}
|
||||
if (norm.startsWith("/")) {
|
||||
return { ...evidence, tier: "verify", note: "仓库外绝对路径,无法在仓库内核实" };
|
||||
}
|
||||
const unindexed = rel.split("/").slice(0, -1).find((seg) => UNINDEXED_DIRS.has(seg.toLowerCase()));
|
||||
if (unindexed) {
|
||||
return { ...evidence, tier: "verify", note: `指向 ${unindexed}/(构建/依赖/配置目录),该目录不纳入清单` };
|
||||
}
|
||||
// Only refs that name a directory can be confirmed: bare filenames resolve
|
||||
// against root, source dir and every basename, so a miss is ambiguous.
|
||||
if (rel.includes("/")) {
|
||||
const alternatives = nearMisses(rootRel, base, comment.file, index);
|
||||
if (alternatives.length) {
|
||||
return { ...evidence, tier: "confirmed", note: `文件疑似被移动/改名,近似候选 ${alternatives.join("、")}`, alternatives };
|
||||
}
|
||||
const dir = [posix.dirname(rootRel), posix.dirname(srcRel)].find(
|
||||
(d) => d !== "." && d !== ".." && !d.startsWith("../") && index.dirs.has(d)
|
||||
);
|
||||
if (dir) {
|
||||
return { ...evidence, tier: "confirmed", note: `目录 ${dir}/ 存在而文件不在,疑似已删除` };
|
||||
}
|
||||
}
|
||||
return { ...evidence, tier: "verify", note: "无法区分仓库外文件与已删除文件" };
|
||||
}
|
||||
|
||||
function checkRefs(comment, index) {
|
||||
const prose = comment.text.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`,。;()]+/gi, " ").replace(/'[^'\n]*'|"[^"\n]*"/g, '""');
|
||||
// Angle-bracketed names ("python3 tool <flow.json>") are argument placeholders.
|
||||
const refs = [...prose.matchAll(REF_RE)]
|
||||
.filter((m) => prose[m.index - 1] !== "<")
|
||||
.map((m) => m[0].replace(/[#?].*$/, ""))
|
||||
.filter((r, i, arr) => arr.indexOf(r) === i)
|
||||
.filter((r) => !r.includes("://") && !/https?:/i.test(r));
|
||||
const dangling = [];
|
||||
const ignoredReferences = [];
|
||||
for (const ref of refs) {
|
||||
const norm = ref.replaceAll("\\", "/");
|
||||
const base = norm.split("/").pop();
|
||||
if (PLACEHOLDER_STEM_RE.test(base.replace(/\.[^.]+$/, ""))) continue;
|
||||
const candidates = [posix.normalize(norm), posix.join(posix.dirname(comment.file), norm)];
|
||||
const hit = candidates.some((c) => index.paths.has(c)) || (!norm.includes("/") && index.basenames.has(base));
|
||||
if (hit) {
|
||||
if (DATED_NAME_RE.test(base)) ignoredReferences.push(ref);
|
||||
} else {
|
||||
dangling.push(gradeDangling(ref, comment, index));
|
||||
}
|
||||
}
|
||||
return { dangling, ignoredReferences };
|
||||
}
|
||||
|
||||
export function changedFiles(repoDir, opts) {
|
||||
return diffContext(repoDir, opts).files;
|
||||
}
|
||||
|
||||
export function headComments(repoDir, relPath) {
|
||||
try {
|
||||
const text = readRevisionFile(repoDir, "HEAD", relPath);
|
||||
return new Set(extractComments(text, relPath).map((c) => c.text.trim()));
|
||||
} catch {
|
||||
return null; // new file or no HEAD version
|
||||
}
|
||||
}
|
||||
|
||||
export function runScan(repoDir, opts = {}) {
|
||||
const invocationDir = resolve(repoDir);
|
||||
repoDir = opts.mode === "diff" ? repositoryRoot(invocationDir) : invocationDir;
|
||||
const config = loadRuleConfig(opts.rule ? resolve(invocationDir, opts.rule) : undefined, repoDir);
|
||||
const scanTarget = opts.path ? resolve(invocationDir, opts.path) : repoDir;
|
||||
const scope = relative(repoDir, scanTarget).replaceAll("\\", "/");
|
||||
if (scope === ".." || scope.startsWith("../") || isAbsolute(scope)) throw new Error("--path must be inside the repository");
|
||||
const severity = (opts.minSeverity ?? "all").toUpperCase();
|
||||
const order = { P0: 0, P1: 1, P2: 2, INFO: 3, ALL: 3 };
|
||||
if (!["P0", "P1", "P2", "ALL"].includes(severity)) throw new Error("Invalid minimum severity");
|
||||
const context = opts.mode === "diff" ? diffContext(repoDir, opts) : null;
|
||||
if (!context?.target && opts.path) {
|
||||
const targetStat = lstatSync(scanTarget);
|
||||
if (targetStat.isSymbolicLink()) throw new Error("--path must not be a symbolic link");
|
||||
}
|
||||
// Reuse this inventory for candidate selection and repository-wide references.
|
||||
const inventory = context?.targetFiles ?? walkFiles(repoDir).map((f) => relative(repoDir, f).replaceAll("\\", "/"));
|
||||
const index = buildFileIndex(inventory);
|
||||
const excludes = config.excludes.map(globToRegExp);
|
||||
const files = (context?.files ?? inventory).filter((f) =>
|
||||
langFor(f) && index.paths.has(f) && (!scope || f === scope || f.startsWith(scope + "/")) &&
|
||||
!f.split("/").slice(0, -1).some((part) => SKIP_DIRS.has(part) || part.toLowerCase() === "vendor") &&
|
||||
!excludes.some((re) => re.test(f))
|
||||
).sort();
|
||||
|
||||
const results = [];
|
||||
const stats = { files: 0, comments: 0, candidates: 0, bySeverity: { P0: 0, P1: 0, P2: 0, INFO: 0 }, byPattern: {} };
|
||||
const ruleGroups = new Map();
|
||||
|
||||
for (const rel of files) {
|
||||
const text = context?.target ? readRevisionFile(repoDir, context.target, rel) : readFileSync(join(repoDir, rel), "utf8");
|
||||
stats.files++;
|
||||
let comments = extractComments(text, rel);
|
||||
if (context?.base && context.baseFiles.has(rel)) {
|
||||
const previous = extractComments(readRevisionFile(repoDir, context.base, rel), rel);
|
||||
comments = newComments(comments, previous);
|
||||
}
|
||||
stats.comments += comments.length;
|
||||
|
||||
let candidates = [];
|
||||
for (const comment of comments) {
|
||||
const { dangling, ignoredReferences } = checkRefs(comment, index);
|
||||
const cls = classifyComment(comment, { ignoredReferences });
|
||||
const confirmed = dangling.filter((d) => d.tier === "confirmed");
|
||||
const unverified = dangling.filter((d) => d.tier === "verify");
|
||||
if (confirmed.length) {
|
||||
const base = cls ?? { ...comment, hits: [], severity: "INFO", reason: "" };
|
||||
base.hits.push({ id: "dangling_ref", weight: "strong", label: "悬空引用(确认失效)", samples: confirmed.map((d) => d.ref).slice(0, 3) });
|
||||
base.severity = "P0";
|
||||
base.reason = `悬空引用:${confirmed.map((d) => d.ref).slice(0, 2).join("、")} 不存在(${confirmed[0].note})`;
|
||||
base.refs = dangling;
|
||||
candidates.push(base);
|
||||
continue;
|
||||
}
|
||||
if (unverified.length) {
|
||||
const base = cls ?? { ...comment, hits: [], severity: "INFO", reason: "" };
|
||||
base.hits.push({ id: "unverified_ref", weight: "medium", label: "引用待核实", samples: unverified.map((d) => d.ref).slice(0, 3) });
|
||||
base.refs = dangling;
|
||||
if (base.severity !== "P0") base.severity = "P1";
|
||||
base.reason = `${cls ? cls.reason + ";" : ""}引用待核实:${unverified.map((d) => d.ref).slice(0, 2).join("、")} 未在仓库找到(${unverified[0].note})`;
|
||||
candidates.push(base);
|
||||
continue;
|
||||
}
|
||||
if (cls) candidates.push(cls);
|
||||
}
|
||||
candidates = candidates.filter((c) => order[c.severity] <= order[severity]);
|
||||
|
||||
if (!candidates.length) continue;
|
||||
const rule = resolveRule(config, rel);
|
||||
const gk = `${rule.source} / ${rule.pattern}`;
|
||||
if (!ruleGroups.has(gk)) ruleGroups.set(gk, { source: rule.source, pattern: rule.pattern, text: rule.text, files: [] });
|
||||
ruleGroups.get(gk).files.push(rel);
|
||||
|
||||
for (const c of candidates) {
|
||||
stats.candidates++;
|
||||
stats.bySeverity[c.severity]++;
|
||||
for (const h of c.hits) stats.byPattern[h.id] = (stats.byPattern[h.id] ?? 0) + 1;
|
||||
results.push({ file: rel, ...c });
|
||||
}
|
||||
}
|
||||
return { repoDir, mode: opts.mode ?? "scan", config, stats, ruleGroups: [...ruleGroups.values()], results };
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
const SEVERITY_LABEL = { P0: "高危(纯过程记录/确认失效引用,删除候选)", P1: "中危(混合/演变叙述或引用待核实,拆分改写/人工核实)", P2: "低危(疑似业务日期,判别后改写)", INFO: "台账(TODO/FIXME)" };
|
||||
|
||||
export function renderMarkdown(spec) {
|
||||
const b = [];
|
||||
b.push(`# Comment Review Spec (${spec.mode})`);
|
||||
b.push("");
|
||||
b.push(`- repo: ${spec.repoDir}`);
|
||||
b.push(`- rules: ${spec.config.source}${spec.config.path ? ` (${spec.config.path})` : " (built-in)"}`);
|
||||
b.push(`- files: ${spec.stats.files} | comments: ${spec.stats.comments} | candidates: ${spec.stats.candidates}`);
|
||||
b.push(`- severity: P0=${spec.stats.bySeverity.P0} P1=${spec.stats.bySeverity.P1} P2=${spec.stats.bySeverity.P2} INFO=${spec.stats.bySeverity.INFO}`);
|
||||
b.push("");
|
||||
|
||||
for (const [i, g] of spec.ruleGroups.entries()) {
|
||||
b.push(`### Comment Rule Group ${i + 1}: ${g.source} / ${g.pattern}`);
|
||||
b.push("");
|
||||
b.push("Applies to:");
|
||||
for (const f of g.files.slice(0, 50)) b.push(`- ${f}`);
|
||||
if (g.files.length > 50) b.push(`- …and ${g.files.length - 50} more`);
|
||||
b.push("");
|
||||
b.push("#### Content");
|
||||
b.push("");
|
||||
b.push(g.text);
|
||||
b.push("");
|
||||
}
|
||||
|
||||
b.push("## Candidates");
|
||||
b.push("");
|
||||
for (const sev of ["P0", "P1", "P2", "INFO"]) {
|
||||
const items = spec.results.filter((r) => r.severity === sev);
|
||||
if (!items.length) continue;
|
||||
b.push(`### ${SEVERITY_LABEL[sev]} — ${items.length} 处`);
|
||||
b.push("");
|
||||
for (const c of items) {
|
||||
const preview = c.text.replaceAll(/\s+/g, " ").slice(0, 160);
|
||||
b.push(`- \`${c.file}:${c.start}-${c.end}\` [${c.reason}] ${preview}${c.text.length > 160 ? "…" : ""}`);
|
||||
if (c.symbol) b.push(` - 挂接: ${c.symbol.replaceAll(/\s+/g, " ").slice(0, 90)}`);
|
||||
if (c.refs) for (const r of c.refs) {
|
||||
b.push(` - 引用: ${r.ref} → ${r.tier === "confirmed" ? "确认失效" : "需核实"}(${r.note};已查 ${r.searched.join("、")})`);
|
||||
}
|
||||
}
|
||||
b.push("");
|
||||
}
|
||||
|
||||
b.push("## Pattern Distribution");
|
||||
b.push("");
|
||||
for (const [k, v] of Object.entries(spec.stats.byPattern).sort((a, b2) => b2[1] - a[1])) {
|
||||
b.push(`- ${k}: ${v}`);
|
||||
}
|
||||
return b.join("\n");
|
||||
}
|
||||
|
||||
export function renderJSON(spec) {
|
||||
return JSON.stringify(
|
||||
{
|
||||
mode: spec.mode,
|
||||
rules: { source: spec.config.source, path: spec.config.path },
|
||||
stats: spec.stats,
|
||||
ruleGroups: spec.ruleGroups,
|
||||
results: spec.results.map(({ hits, ...rest }) => ({ ...rest, hits: hits.map((h) => ({ id: h.id, samples: h.samples })) })),
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { runScan } from "../src/scan.js";
|
||||
import { extractComments } from "../src/extract.js";
|
||||
|
||||
// Corpus lives outside test/ so `node --test` never executes the sample
|
||||
// sources themselves (TS samples would load as tests on newer Node).
|
||||
const corpusDir = fileURLToPath(new URL("../testdata/corpus", import.meta.url));
|
||||
|
||||
function buildCorpusRepo(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), "cmr-corpus-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
mkdirSync(join(root, ".commentreview"), { recursive: true });
|
||||
writeFileSync(join(root, ".commentreview", "rule.json"), '{"rules":[]}');
|
||||
cpSync(corpusDir, root, {
|
||||
recursive: true,
|
||||
filter: (src) => !(dirname(src) === corpusDir && ["expected.json", "README.md"].includes(basename(src))),
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
test("human-judged corpus: no false positives, no missed detections", (t) => {
|
||||
const root = buildCorpusRepo(t);
|
||||
const expected = JSON.parse(readFileSync(join(corpusDir, "expected.json"), "utf8"));
|
||||
const spec = runScan(root, {});
|
||||
const flagged = new Map(spec.results.map((r) => [`${r.file}:${r.start}`, r]));
|
||||
|
||||
const commentKeys = new Set();
|
||||
for (const e of expected) {
|
||||
const text = readFileSync(join(root, e.file), "utf8");
|
||||
for (const c of extractComments(text, e.file)) commentKeys.add(`${e.file}:${c.start}`);
|
||||
}
|
||||
|
||||
const mismatches = [];
|
||||
const scorecard = {};
|
||||
for (const e of expected) {
|
||||
const key = `${e.file}:${e.line}`;
|
||||
assert.ok(commentKeys.has(key), `stale expectation: ${key} does not point at a comment`);
|
||||
const hit = flagged.get(key);
|
||||
const score = (scorecard[e.category] ??= { total: 0, clean: 0 });
|
||||
score.total++;
|
||||
if (e.severity === null && hit) mismatches.push(`FP ${key} [${e.category}] 应不告警: ${hit.reason}`);
|
||||
else if (e.severity !== null && !hit) mismatches.push(`FN ${key} [${e.category}] 期望 ${e.severity},未检出`);
|
||||
else if (e.severity !== null && hit.severity !== e.severity) mismatches.push(`SEV ${key} [${e.category}] 期望 ${e.severity},实际 ${hit.severity}: ${hit.reason}`);
|
||||
else score.clean++;
|
||||
}
|
||||
const summary = Object.entries(scorecard).map(([cat, s]) => `${cat} ${s.clean}/${s.total}`).join(",");
|
||||
console.log(`corpus scorecard: ${summary}${mismatches.length ? "" : " — 判定全部符合"}`);
|
||||
assert.deepEqual(mismatches, [], `corpus 判定回归(FP=误报 FN=漏报 SEV=级别不符):\n${mismatches.join("\n")}`);
|
||||
});
|
||||
|
||||
test("corpus covers every scanned sample file", (t) => {
|
||||
// Files holding samples must appear in expectations; support files under
|
||||
// docs/, shared/ and api/ exist only to shape reference evidence.
|
||||
const expected = JSON.parse(readFileSync(join(corpusDir, "expected.json"), "utf8"));
|
||||
const covered = new Set(expected.map((e) => e.file));
|
||||
for (const f of ["CorpusCi.yml", "CorpusExternal.ts", "CorpusForm.vue", "CorpusFields.java", "CorpusFront.ts",
|
||||
"CorpusMisc.sql", "CorpusMoved.java", "CorpusRefs.java", "CorpusServer.java", "CorpusUsage.sh", "CorpusWorkflow.java"]) {
|
||||
assert.ok(covered.has(f), `${f} has no judgment recorded`);
|
||||
}
|
||||
});
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
package com.example;
|
||||
|
||||
/**
|
||||
* 车辆申请服务
|
||||
* 修订记录:
|
||||
* 2026-05-12 初版
|
||||
* 2026-06-03 按张总要求增加审批环节
|
||||
*/
|
||||
public class VehicleService {
|
||||
|
||||
// 2026-07-15 修改:原来走线下审批,8月评审驳回后改为线上流程
|
||||
public void submit(VehicleBo bo) {
|
||||
check(bo);
|
||||
}
|
||||
|
||||
/** 金额必须大于 0:自 2026 年 3 月起甲方对负数金额有对账纠纷 */
|
||||
private void check(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// TODO: 等后端接口上线后补齐校验 2026-09-01
|
||||
public void validate(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 先这样实现,后续会重构,李工说暂时够用
|
||||
public String buildNo(VehicleBo bo) {
|
||||
return "V" + bo.getId();
|
||||
}
|
||||
|
||||
// 绑定口径按 HANDLER-LEDGER.md 4.3(该文档已删除)
|
||||
public void bind(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 实测案例:594s click 挂死,500ms 等待有收益
|
||||
public void click(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 结构版本 v2 起改为批量回填;旧版本逐条更新已被 9edfb29 取代
|
||||
public void backfill(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// supplies 全流程辅助(阶段 3),T3 交付范围内
|
||||
public void assist(VehicleBo bo) {
|
||||
}
|
||||
|
||||
/** 偶发超过 15s(当前环境频率,支撑 30s 超时取值);动画 300ms */
|
||||
public void poll(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 口径见 docs/dated-2026-09-12-matrix.md(真实存在的带日期文档,按实际文件名保留)
|
||||
public void matrix(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// FWGL 发布定义 v6 起为该编码(外部版本锚定 = 业务约束,保留)
|
||||
public void anchor(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 旧版流程实例仍在流转,归档后方可清理(业务事实 = 移除前提,保留)
|
||||
public void legacy(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 结构版本 v2 起改为校验前置(自身演变 → 改写为当前语义)
|
||||
public void reorder(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 自 427a61c 适配收文分流(无前缀提交溯源)
|
||||
public void importFrom(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 不再逐处校验,改为网关统一拦截
|
||||
public void gate(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 删除合同变更记录(CRUD 功能注释,非候选)
|
||||
public void deleteChangeByName(VehicleBo bo) {
|
||||
}
|
||||
|
||||
// 连接 127.0.0.1:6379 的 Redis(IP 不触发版本模式)
|
||||
public void connect(VehicleBo bo) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# 环境矩阵(带日期的真实文档样例)
|
||||
@@ -0,0 +1,232 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { extractComments } from "../src/extract.js";
|
||||
import { classifyComment } from "../src/patterns.js";
|
||||
import { runScan } from "../src/scan.js";
|
||||
import { loadRuleConfig, resolveRule } from "../src/rules.js";
|
||||
import { matchGlob } from "../src/glob.js";
|
||||
|
||||
const cli = fileURLToPath(new URL("../bin/cmr.js", import.meta.url));
|
||||
function fixture(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), "cmr-test-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const write = (path, text) => {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true });
|
||||
writeFileSync(join(root, path), text);
|
||||
};
|
||||
write(".commentreview/rule.json", '{"rules":[]}');
|
||||
const git = (...args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
||||
const commit = () => {
|
||||
git("add", ".");
|
||||
git("-c", "user.name=Test", "-c", "user.email=test@localhost", "commit", "-qm", "fixture");
|
||||
return git("rev-parse", "HEAD");
|
||||
};
|
||||
return { root, write, git, commit };
|
||||
}
|
||||
|
||||
test("documented lowercase severity retains P0 findings", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("a.js", "// 定稿\nconst a = 1;");
|
||||
assert.equal(runScan(root, { minSeverity: "p0" }).stats.candidates, 1);
|
||||
});
|
||||
|
||||
for (const args of [["scan", "--format", "jsoon"], ["scan", "--min-severity", "bad"], ["scan", "--path"], ["scan", "--unknown"], ["diff", "--from", "main"], ["scan", "--commit", "HEAD"], ["rules"]]) {
|
||||
test(`CLI rejects invalid arguments: ${args.join(" ")}`, (t) => {
|
||||
const { root } = fixture(t);
|
||||
const result = spawnSync(process.execPath, [cli, ...args], { cwd: root, encoding: "utf8" });
|
||||
assert.equal(result.status, 2, result.stderr);
|
||||
});
|
||||
}
|
||||
|
||||
test("rules does not interpret option values as file names", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("custom.json", '{"rules":[{"path":"**","rule":"custom"}]}');
|
||||
const result = spawnSync(process.execPath, [cli, "rules", "src/a.js", "--rule", "custom.json"], { cwd: root, encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout.split("\n").filter((line) => line.includes("->")).length, 1);
|
||||
});
|
||||
|
||||
test("explicit missing and malformed rules fail instead of silently falling back", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
assert.throws(() => loadRuleConfig("missing.json", root), /rule|规则/i);
|
||||
write("bad.json", '{"rules":[{"path":42}]}');
|
||||
assert.throws(() => loadRuleConfig("bad.json", root), /rule|规则/i);
|
||||
});
|
||||
|
||||
test("project excludes and vendor exclusions affect files and statistics", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write(".commentreview/rule.json", '{"rules":[],"excludes":["ignored/**"]}');
|
||||
for (const path of ["a.js", "ignored/b.js", "vendor/c.js", "src/vendor/d.js"]) write(path, "// 定稿");
|
||||
const spec = runScan(root, {});
|
||||
assert.equal(spec.stats.files, 1);
|
||||
assert.deepEqual(spec.results.map((r) => r.file), ["a.js"]);
|
||||
});
|
||||
|
||||
test("symlink directories cannot recurse back into the scan", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("src/a.js", "// 定稿");
|
||||
symlinkSync(root, join(root, "loop"), process.platform === "win32" ? "junction" : "dir");
|
||||
assert.equal(runScan(root, {}).stats.files, 1);
|
||||
});
|
||||
|
||||
test("reference URLs are not missing local files; explicit paths cannot match another basename", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("docs/guide.md", "guide");
|
||||
write("a.js", "// https://example.org/missing-guide.md\nconst a = 1;\n// see missing/guide.md\nconst b = 2;");
|
||||
const results = runScan(root, {}).results;
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].start, 3);
|
||||
assert.equal(results[0].hits[0].id, "dangling_ref");
|
||||
});
|
||||
|
||||
test("dated reference exemption does not erase a separate change date", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("docs/dated-2026-09-12.md", "guide");
|
||||
write("a.js", "// see docs/dated-2026-09-12.md\nconst a = 1;\n// 2026-09-13 修改实现,见 docs/dated-2026-09-12.md\nconst b = 2;");
|
||||
const results = runScan(root, {}).results;
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].severity, "P0");
|
||||
});
|
||||
|
||||
test("TODO alone belongs in the INFO ledger", () => {
|
||||
assert.equal(classifyComment({ text: "TODO: 补齐校验" })?.severity, "INFO");
|
||||
assert.equal(classifyComment({ text: "新增合同变更记录" }), null);
|
||||
assert.equal(classifyComment({ text: "张总:需要调整流程" })?.severity, "P1");
|
||||
});
|
||||
|
||||
test("extractor skips escaped quotes and multiline string contents", () => {
|
||||
const source = String.raw`const a = "escaped \" // 定稿";`;
|
||||
assert.equal(extractComments(source.split("\n")[0], "a.js").length, 0);
|
||||
assert.deepEqual(extractComments('const x = `first\n// 定稿\nlast`;\n// TODO: real', "a.js").map((c) => c.text.trim()), ["TODO: real"]);
|
||||
});
|
||||
|
||||
test("extractor recognizes Javadoc and Python docstrings", () => {
|
||||
const docs = extractComments("/** API contract */\nclass A {}", "A.java");
|
||||
assert.equal(docs[0].kind, "doc");
|
||||
assert.equal(docs[0].text.trim(), "API contract");
|
||||
assert.equal(extractComments('"""Module contract"""\nx = 1', "a.py")[0]?.text, "Module contract");
|
||||
assert.equal(extractComments('x = """data\n# 定稿\n"""', "a.py").length, 0);
|
||||
});
|
||||
|
||||
test("hash markers respect properties and YAML syntax", () => {
|
||||
assert.equal(extractComments("password=abc#secret!value", "a.properties").length, 0);
|
||||
assert.equal(extractComments("url: https://example.org/#fragment", "a.yaml").length, 0);
|
||||
assert.equal(extractComments("# TODO: real", "a.properties").length, 1);
|
||||
});
|
||||
|
||||
test("glob and rule priority preserve root files and Windows separators", () => {
|
||||
assert.ok(matchGlob("**/*.js", "a.js"));
|
||||
assert.ok(matchGlob("src/*.js", "src\\a.js"));
|
||||
assert.ok(!matchGlob("src/*.js", "src/nested/a.js"));
|
||||
const rule = resolveRule({ source: "project", rules: [{ path: "**", rule: "only", merge_system_rule: false }] }, "a.js");
|
||||
assert.equal(rule.text, "only");
|
||||
});
|
||||
|
||||
test("diff reads the selected commit and branch snapshot, not dirty worktree files", (t) => {
|
||||
const { root, write, git, commit } = fixture(t);
|
||||
git("init", "-q");
|
||||
write("中文 file.js", "// 定稿\nconst a = 1;\n");
|
||||
const base = commit();
|
||||
write("中文 file.js", "// 定稿\nconst a = 2;\n// 返工\nconst b = 1;\n");
|
||||
const target = commit();
|
||||
write("中文 file.js", "// 废弃\nconst dirty = true;\n");
|
||||
for (const options of [{ commit: target }, { from: base, to: target }]) {
|
||||
const spec = runScan(root, { mode: "diff", ...options });
|
||||
assert.deepEqual(spec.results.map((r) => r.text.trim()), ["返工"]);
|
||||
assert.equal(spec.results[0].file, "中文 file.js");
|
||||
assert.equal(spec.results[0].start, 3);
|
||||
}
|
||||
});
|
||||
|
||||
test("diff supports initial commit, unborn HEAD, path scope and duplicate comments", (t) => {
|
||||
const { root, write, git, commit } = fixture(t);
|
||||
git("init", "-q");
|
||||
write("src/a.js", "// 定稿\nconst a = 1;\n");
|
||||
assert.equal(runScan(root, { mode: "diff" }).stats.candidates, 1);
|
||||
const first = commit();
|
||||
assert.equal(runScan(root, { mode: "diff", commit: first }).stats.candidates, 1);
|
||||
write("src/a.js", "// 定稿\nconst a = 1;\n// 定稿\nconst b = 2;\n");
|
||||
write("other/b.js", "// 返工\n");
|
||||
const spec = runScan(root, { mode: "diff", path: "src" });
|
||||
assert.deepEqual(spec.results.map((r) => r.file.replaceAll("\\", "/")), ["src/a.js"]);
|
||||
assert.equal(spec.results[0].start, 3);
|
||||
assert.equal(runScan(join(root, "src"), { mode: "diff", path: "." }).stats.candidates, 1);
|
||||
});
|
||||
|
||||
test("historical reference resolution uses the target tree", (t) => {
|
||||
const { root, write, git, commit } = fixture(t);
|
||||
git("init", "-q");
|
||||
write("a.js", "const a = 1;\n");
|
||||
const base = commit();
|
||||
write("a.js", "// see docs/dated-2026-09-12.md\nconst a = 1;\n");
|
||||
write("docs/dated-2026-09-12.md", "guide");
|
||||
const target = commit();
|
||||
rmSync(join(root, "docs"), { recursive: true });
|
||||
assert.equal(runScan(root, { mode: "diff", from: base, to: target }).stats.candidates, 0);
|
||||
});
|
||||
|
||||
test("scan rejects a nonexistent path instead of returning a clean report", (t) => {
|
||||
const { root } = fixture(t);
|
||||
assert.throws(() => runScan(root, { path: "missing" }), /ENOENT/);
|
||||
assert.throws(() => runScan(root, { path: ".." }), /inside/);
|
||||
});
|
||||
|
||||
test("unchanged comments, deleted files and code-only changes produce no diff candidates", (t) => {
|
||||
const { root, write, git, commit } = fixture(t);
|
||||
git("init", "-q");
|
||||
write("a.js", "// 定稿\nconst a = 1;\n");
|
||||
write("b.js", "// 返工\n");
|
||||
commit();
|
||||
write("a.js", "// 定稿\nconst a = 2;\n");
|
||||
git("rm", "-q", "b.js");
|
||||
assert.equal(runScan(root, { mode: "diff" }).stats.candidates, 0);
|
||||
const target = commit();
|
||||
assert.equal(runScan(root, { mode: "diff", commit: target }).stats.candidates, 0);
|
||||
});
|
||||
|
||||
test("branch range uses merge base even when the source branch has advanced", (t) => {
|
||||
const { root, write, git, commit } = fixture(t);
|
||||
git("init", "-q");
|
||||
write("a.js", "const a = 1;\n");
|
||||
const base = commit();
|
||||
write("a.js", "// 定稿\nconst a = 1;\n");
|
||||
const target = commit();
|
||||
git("checkout", "--detach", "-q", base);
|
||||
write("a.js", "// 返工\nconst a = 1;\n");
|
||||
const from = commit();
|
||||
const results = runScan(root, { mode: "diff", from, to: target }).results;
|
||||
assert.deepEqual(results.map((r) => r.text.trim()), ["定稿"]);
|
||||
});
|
||||
|
||||
test("a source file with no comments is counted without findings", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("a.js", 'const url = "https://example.org";\n');
|
||||
const { stats } = runScan(root, {});
|
||||
assert.equal(stats.files, 1);
|
||||
assert.equal(stats.comments, 0);
|
||||
assert.equal(stats.candidates, 0);
|
||||
});
|
||||
|
||||
test("rules JSON contains resolved content and uses only positional files", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("custom.json", '{"rules":[{"path":"**","rule":"custom","merge_system_rule":false}]}');
|
||||
const result = spawnSync(process.execPath, [cli, "rules", "a.js", "--rule", "custom.json", "--format", "json"], { cwd: root, encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(JSON.parse(result.stdout), [{ file: "a.js", source: "cli", pattern: "**", text: "custom" }]);
|
||||
});
|
||||
|
||||
test("original Java examples retain business constraints and report process history", () => {
|
||||
const root = fileURLToPath(new URL("../", import.meta.url));
|
||||
const { stats, results } = runScan(root, { path: "test/fixtures" });
|
||||
assert.equal(stats.comments, 18);
|
||||
assert.equal(stats.candidates, 12);
|
||||
// HANDLER-LEDGER.md is a bare filename with no near miss: unverifiable, so
|
||||
// it rides at P1 for the agent instead of a P0 deletion verdict.
|
||||
assert.deepEqual(stats.bySeverity, { P0: 2, P1: 9, P2: 0, INFO: 1 });
|
||||
for (const line of [45, 49, 53, 57, 73, 77]) assert.ok(!results.some((r) => r.start === line), `business/example comment at ${line}`);
|
||||
});
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
# 单次完成测试与打包;后续 job 直接复用三个 JAR,不再重复编译。
|
||||
stages: [build]
|
||||
|
||||
build-job:
|
||||
script: [make]
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
// 规则分层:CLI --rule > 项目 .commentreview/rule.json > 用户 ~/.commentreview/rule.json > 内置
|
||||
export const layering = true;
|
||||
|
||||
// pnpm 跳过了 postinstall 脚本,lib/index.js 未生成,故直接引用 v3(对应 Vue3) 构建产物
|
||||
declare module "@vue-office/pdf";
|
||||
|
||||
// 用法: python3 validate <flow.json>(尖括号占位符不算文件引用)
|
||||
export const usage = "validate <flow.json>";
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
package corpus;
|
||||
|
||||
/** 字段枚举与格式说明(数据事实,保留)。 */
|
||||
public class CorpusFields {
|
||||
|
||||
/** 外文版本:1是,0否 */
|
||||
private Integer foreignVersion;
|
||||
|
||||
// 例如,2023年7月可以表示为 "2023-07"
|
||||
public static final String MONTH_FORMAT = "yyyy-MM";
|
||||
|
||||
// 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
|
||||
public void batchInsert() {}
|
||||
|
||||
/** 操作来源(字典 supplies_stock_op:manual=手工 inventory=盘点 purchase=采购 initial=初始) */
|
||||
private String opSource;
|
||||
|
||||
// 考核月份快捷搜索,格式 YYYY-MM,如 2026-08
|
||||
public static final String MONTH_QUERY = "yyyy-MM";
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
<!-- 修改已关联的合同名称即切换到手工模式,原台账字段不再适用。 -->
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
<script>
|
||||
// 保存业务数据(草稿/编辑模式)。审批中的业务回写走专用办理接口,不再整表单提交。
|
||||
export function saveBusinessData() {}
|
||||
</script>
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
// 兼容旧版静态路由 key(避免 meta.title 使用 dashboard/document 时无法命中翻译)
|
||||
export const routeKeyMap: Record<string, string> = {};
|
||||
|
||||
// 兼容旧版浏览器,避免静默报错阻塞
|
||||
export function notify(): void {}
|
||||
|
||||
// 极少数浏览器(如旧版 360)不支持 Promise 形式,降级尝试回调版
|
||||
export function fallbackNotify(): void {}
|
||||
|
||||
// 后端已关闭多租户,不再请求租户列表接口 initTenantList();
|
||||
export function login(): void {}
|
||||
|
||||
// 搜索和切换页签会主动刷新;此处不再监听分页变量,避免重复请求。
|
||||
export function onTabSwitch(): void {}
|
||||
|
||||
// 优先匹配方法 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口
|
||||
export function hasAnnotation(): boolean { return true; }
|
||||
|
||||
// 偶发超过 15s(当前环境频率,支撑 30s 超时取值);动画 300ms
|
||||
export const TIMEOUT = 30000;
|
||||
|
||||
// 领导批示流转节点:自定义选人弹窗(选副总经理 + 相关部门)
|
||||
export function selectDeputy(): void {}
|
||||
|
||||
// 副总承办开关联动:选"否"时清空已选副总经理
|
||||
export function clearDeputy(): void {}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
-- 设计说明(最佳实践,对照请假 test_leave / 发文 oa_document_send.sql):
|
||||
-- 1. 审批意见不入业务表,统一由 warm-flow flow_his_task.message 记录;
|
||||
-- 2. 名称字段不入表,一律按 ID 存,由 ServiceImpl#fillNames 运行时回填到 VO;
|
||||
CREATE TABLE t_design (id NUMBER(20));
|
||||
|
||||
-- ZHGL 印章基础功能:字典 ID: seal_type=104, seal_status=105, data=1032-1039
|
||||
CREATE TABLE t_seal (id NUMBER(20));
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
package corpus;
|
||||
|
||||
/** 结构性证据支撑的确认失效引用(P0):目录存在/近似候选。 */
|
||||
public class CorpusMoved {
|
||||
|
||||
// 接口约定见 api/contract.md(该文档已迁移)
|
||||
public void client() {}
|
||||
|
||||
// 部署步骤见 docs/deploy.md
|
||||
public void deploy() {}
|
||||
|
||||
// 口径见 docs/dated-2026-09-12-matrix.md(真实存在的带日期文档,按实际文件名保留)
|
||||
public void matrix() {}
|
||||
}
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
package corpus;
|
||||
|
||||
/** 裸文件名/跨仓引用:仓库内无法核实,报 P1 待核实而非 P0 失效。 */
|
||||
public class CorpusRefs {
|
||||
|
||||
// 绑定口径按 HANDLER-LEDGER.md 4.3(该文档已删除)
|
||||
public void bind() {}
|
||||
|
||||
// 系统管理员(user_id=1) 为内置账号,保留于此;其余用户已脱敏,转移至 sys_user_data.sql
|
||||
public void initUsers() {}
|
||||
|
||||
// 0、warm-flow-all.sql
|
||||
public void importWorkflow() {}
|
||||
|
||||
// 字典:attendance_mark 考勤标记 dictValue 与前端 attendanceConfig.ts 的映射注释对齐(休=3/病=5/事=6)
|
||||
public void dict() {}
|
||||
|
||||
// 注意:返回 DocumentSendVo 而非 Boolean。原因:前端 sendForm.vue 提交后需要获取 id 用于发起工作流
|
||||
public Object add() { return null; }
|
||||
|
||||
// 业务选择器目录按钮(用户/部门/岗位,12040-12042):15 个基线角色全量绑定(印章选择按钮 12043 的绑定随 04_seal.sql)
|
||||
public void bindRoles() {}
|
||||
|
||||
// 匹配views里面所有的.vue文件
|
||||
public Object modules() { return null; }
|
||||
}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
package corpus;
|
||||
|
||||
/**
|
||||
* 车辆申请服务
|
||||
* 修订记录:
|
||||
* 2026-05-12 初版
|
||||
* 2026-06-03 按张总要求增加审批环节
|
||||
*/
|
||||
public class CorpusServer {
|
||||
|
||||
// 2026-07-15 修改:原来走线下审批,8月评审驳回后改为线上流程
|
||||
public void submit() {}
|
||||
|
||||
/** 金额必须大于 0:自 2026 年 3 月起甲方对负数金额有对账纠纷 */
|
||||
private void check() {}
|
||||
|
||||
// 先这样实现,后续会重构,李工说暂时够用
|
||||
public String buildNo() { return ""; }
|
||||
|
||||
// 实测案例:594s click 挂死,500ms 等待有收益
|
||||
public void click() {}
|
||||
|
||||
// 结构版本 v2 起改为批量回填;旧版本逐条更新已被 9edfb29 取代
|
||||
public void backfill() {}
|
||||
|
||||
// 自 427a61c 适配收文分流
|
||||
public void importFrom() {}
|
||||
|
||||
// supplies 全流程辅助(阶段 3),T3 交付范围内
|
||||
public void assist() {}
|
||||
|
||||
// TODO: 等后端接口上线后补齐校验 2026-09-01
|
||||
public void validate() {}
|
||||
|
||||
// 列表数据范围由角色 data_scope 决定(@DataPermission),不再硬编码仅本人
|
||||
public void list() {}
|
||||
|
||||
// 提出单位已经改为字典项,后端保存并透传 dictValue,不再限定旧的中文展示文本。
|
||||
public void save() {}
|
||||
|
||||
// 台账编辑只维护六月原型中的起止日期、履约情况、备注和附件。支付列表由支付审批办结后自动写入,台账编辑不再删除或重建支付记录。
|
||||
public void editLedger() {}
|
||||
|
||||
// 从 springboot 3.5 开始 spring自带线程池 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用
|
||||
public void configureAsync() {}
|
||||
|
||||
// 连接 127.0.0.1:6379 的 Redis(IP 不触发版本模式)
|
||||
public void connect() {}
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
# ./ry.sh start 启动 stop 停止 restart 重启 status 状态
|
||||
case "$1" in
|
||||
start) echo start ;;
|
||||
esac
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
package corpus;
|
||||
|
||||
/** 业务对象版本事实样例(应保留,不判为过程叙述)。 */
|
||||
public class CorpusWorkflow {
|
||||
|
||||
// 旧版据此选择部门承办或副总办理;新版据此决定合并办理节点是否包含副总。
|
||||
private Integer defVersion;
|
||||
|
||||
// 新版收文定义据此决定合并办理节点是否包含副总,发起时固定 N,由后续节点按需调整
|
||||
public void startProcess() {}
|
||||
|
||||
// 旧版收文定义的业务节点编码,旧实例仍在流转时返回这些编码;确认不存在可继续办理的旧实例后方可移除。
|
||||
public String legacyNodeCode() { return ""; }
|
||||
|
||||
// 新版钉钉二维码登录
|
||||
public void dingLogin() {}
|
||||
|
||||
// ② 旧版部门承办入口:任务校验失败无写入。
|
||||
public void legacyEntry() {}
|
||||
|
||||
// 任务节点不在集合内(如旧版节点码已收窄)→ 抛「与业务/节点不匹配」。
|
||||
public void nodeMismatch() {}
|
||||
|
||||
// 部门承办人用户 ID 列表(写入业务单并作为后续会签办理人)
|
||||
private java.util.List<Long> deptUserIds;
|
||||
}
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
# 人工判定样例库(corpus)
|
||||
|
||||
带人工判定期望结果的注释样例库,用于衡量 cmr 的**误报**与**漏报**,防止调一条规则误伤另一类注释。功能正确性由 `test/review.test.js` 覆盖;本库回答的是"判定对不对"。
|
||||
|
||||
## 样例来源
|
||||
|
||||
绝大多数样例摘自 zhgl / zhywgl 前后端四个真实仓库的审查报告(`reports/*.md`),少量为支撑判定路径的合成样例(`CorpusMoved.java` 及 `docs/`、`shared/`、`api/` 下的支撑文件)。
|
||||
|
||||
## 分类与期望
|
||||
|
||||
`expected.json` 每条目:`{ file, line, category, severity, note }`。`line` 为注释起始行;`severity: null` 表示**不得**列为候选。
|
||||
|
||||
| category | 含义 | 期望 |
|
||||
|---|---|---|
|
||||
| `process_record` | 过程记录(日期戳、评审历程、归因、演变叙述、确认失效引用) | 必须列为候选,且级别一致 |
|
||||
| `business_constraint` | 业务约束(生效日、兼容要求、版本化业务对象、库能力锚定) | 不得列为候选(误报即回归) |
|
||||
| `normal_note` | 正常说明(数据事实、连接示例) | 不得列为候选 |
|
||||
| `needs_verify` | 待核实(外部路径、构建产物、跨仓/裸文件名、混中文描述) | 必须以 P1 待核实列出,附查找依据与近似候选 |
|
||||
|
||||
## 维护规则
|
||||
|
||||
1. 新增样例:在 `Corpus*.java|ts|vue|sql|yml|sh` 中追加注释(用代码行隔开,避免合并成一条),在 `expected.json` 登记 `file:line` 与人工判定。判定必须先于工具输出写下——先跑工具再抄结果是循环论证。
|
||||
2. `test/corpus.test.js` 会校验:每条期望都指向真实注释;判定完全一致。失败时按记分卡逐条定位(FP=误报、FN=漏报、SEV=级别不符)。
|
||||
3. 判定本身可修订:如果事后认定某条 `severity` 判错了,改 `expected.json` 并在 `note` 写明理由,与当次提交一起评审。
|
||||
4. 样例库**不放**争议样例(如 SQL 迁移文件头部的版本声明):没有把握的判定进不了门禁。
|
||||
|
||||
`docs/`、`shared/`、`api/` 下的支撑文件用于制造引用判定的结构性证据(目录存在、同名文件在别处、真实带日期文档),勿删。
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// api 目录存在性判定样例
|
||||
export const other = 1;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// E2E 环境角色基线。必须在 test.config.json 中显式配置 roleKeys,不根据当前角色是否"看起来够用"放行。
|
||||
// 双胞胎副本:zhywgl-e2e/test/common/role-baseline.ts(E2E health 前置使用)与本文件内容保持一致
|
||||
export function roleBaseline(): void {}
|
||||
@@ -0,0 +1,3 @@
|
||||
# 口径矩阵(带日期文档样例)
|
||||
|
||||
真实存在、文件名本身带日期的文档,引用应按实际文件名保留。
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# 部署手册
|
||||
|
||||
支撑目录存在性判定的示例文档(deploy.md 本身不存在)。
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{ "file": "CorpusCi.yml", "line": 1, "category": "process_record", "severity": "P1", "note": "CI 行为说明含\"不再重复编译\"过程尾巴,拆分改写" },
|
||||
{ "file": "CorpusExternal.ts", "line": 1, "category": "needs_verify", "severity": "P1", "note": "可选配置与用户家目录路径,仓库内无法核实" },
|
||||
{ "file": "CorpusExternal.ts", "line": 4, "category": "needs_verify", "severity": "P1", "note": "lib/ 构建产物,postinstall 生成,不在清单" },
|
||||
{ "file": "CorpusExternal.ts", "line": 7, "category": "normal_note", "severity": null, "note": "用法行的尖括号占位符不是文件引用" },
|
||||
{ "file": "CorpusForm.vue", "line": 1, "category": "process_record", "severity": "P1", "note": "行为说明+\"原台账字段不再适用\"历史尾巴" },
|
||||
{ "file": "CorpusForm.vue", "line": 6, "category": "process_record", "severity": "P1", "note": "保存行为说明+\"不再整表单提交\"历史尾巴" },
|
||||
{ "file": "CorpusFields.java", "line": 6, "category": "business_constraint", "severity": null, "note": "字段枚举值文档:外文版本 1是 0否" },
|
||||
{ "file": "CorpusFields.java", "line": 9, "category": "business_constraint", "severity": null, "note": "格式示例的日期是教学值,不是变更戳" },
|
||||
{ "file": "CorpusFields.java", "line": 12, "category": "business_constraint", "severity": null, "note": "MyBatis-Plus 3.5.0 库能力说明,外部版本锚定" },
|
||||
{ "file": "CorpusFields.java", "line": 15, "category": "business_constraint", "severity": null, "note": "字典枚举文档:manual=手工,手工/人工是操作方式不是人名称谓" },
|
||||
{ "file": "CorpusFields.java", "line": 18, "category": "business_constraint", "severity": null, "note": "\"如 2026-08\"是格式示例值,不是变更日期" },
|
||||
{ "file": "CorpusFront.ts", "line": 1, "category": "business_constraint", "severity": null, "note": "兼容旧版路由 key 是存活兼容要求" },
|
||||
{ "file": "CorpusFront.ts", "line": 4, "category": "business_constraint", "severity": null, "note": "兼容旧版浏览器是支持性约束" },
|
||||
{ "file": "CorpusFront.ts", "line": 7, "category": "business_constraint", "severity": null, "note": "旧版 360 是外部产品版本,降级方案为现状" },
|
||||
{ "file": "CorpusFront.ts", "line": 10, "category": "process_record", "severity": "P1", "note": "现状+\"不再请求租户列表\"历史尾巴,拆分" },
|
||||
{ "file": "CorpusFront.ts", "line": 13, "category": "process_record", "severity": "P1", "note": "刷新行为说明+\"不再监听分页变量\"尾巴" },
|
||||
{ "file": "CorpusFront.ts", "line": 16, "category": "process_record", "severity": "P1", "note": "匹配规则说明+\"不再往上匹配\"尾巴,改写为\"仅检查当前方法\"" },
|
||||
{ "file": "CorpusFront.ts", "line": 19, "category": "business_constraint", "severity": null, "note": "环境频率现状支撑超时取值" },
|
||||
{ "file": "CorpusFront.ts", "line": 22, "category": "business_constraint", "severity": null, "note": "选副总经理是表单角色选择,副总为业务角色词非人名称谓" },
|
||||
{ "file": "CorpusFront.ts", "line": 25, "category": "business_constraint", "severity": null, "note": "副总承办开关联动为业务规则,同上" },
|
||||
{ "file": "CorpusMisc.sql", "line": 1, "category": "needs_verify", "severity": "P1", "note": "裸文件名对照示例,跨仓/外部脚本无法区分" },
|
||||
{ "file": "CorpusMisc.sql", "line": 6, "category": "normal_note", "severity": null, "note": "字典 ID 数据事实" },
|
||||
{ "file": "CorpusMoved.java", "line": 6, "category": "process_record", "severity": "P0", "note": "确认失效:同名文件在 shared/,路径未更新" },
|
||||
{ "file": "CorpusMoved.java", "line": 9, "category": "process_record", "severity": "P0", "note": "确认失效:docs/ 目录存在而 deploy.md 不在" },
|
||||
{ "file": "CorpusMoved.java", "line": 12, "category": "business_constraint", "severity": null, "note": "引用真实存在的带日期文档,按实际文件名保留" },
|
||||
{ "file": "CorpusRefs.java", "line": 6, "category": "needs_verify", "severity": "P1", "note": "裸文件名自称已删除,工具无法确认,交 agent 定夺" },
|
||||
{ "file": "CorpusRefs.java", "line": 9, "category": "needs_verify", "severity": "P1", "note": "脱敏数据转移到仓库外文件" },
|
||||
{ "file": "CorpusRefs.java", "line": 12, "category": "needs_verify", "severity": "P1", "note": "第三方 warm-flow 的外部脚本" },
|
||||
{ "file": "CorpusRefs.java", "line": 15, "category": "needs_verify", "severity": "P1", "note": "前端仓文件,跨仓引用合法" },
|
||||
{ "file": "CorpusRefs.java", "line": 18, "category": "needs_verify", "severity": "P1", "note": "前端仓组件,跨仓引用合法" },
|
||||
{ "file": "CorpusRefs.java", "line": 21, "category": "needs_verify", "severity": "P1", "note": "发布序列内其他脚本,当前树缺失需人工核实" },
|
||||
{ "file": "CorpusRefs.java", "line": 24, "category": "needs_verify", "severity": "P1", "note": "中文混排描述被误读为路径,标记待核实而非失效" },
|
||||
{ "file": "CorpusServer.java", "line": 3, "category": "process_record", "severity": "P0", "note": "修订记录标题+日期+人物归因,纯过程记录" },
|
||||
{ "file": "CorpusServer.java", "line": 11, "category": "process_record", "severity": "P0", "note": "日期+驳回/改为过程叙述" },
|
||||
{ "file": "CorpusServer.java", "line": 14, "category": "process_record", "severity": "P1", "note": "业务生效日是约束,但\"甲方\"归因需抹除,拆分" },
|
||||
{ "file": "CorpusServer.java", "line": 17, "category": "process_record", "severity": "P1", "note": "临时方案+人物归因" },
|
||||
{ "file": "CorpusServer.java", "line": 20, "category": "process_record", "severity": "P1", "note": "实测回忆" },
|
||||
{ "file": "CorpusServer.java", "line": 23, "category": "process_record", "severity": "P1", "note": "版本演变起点+提交溯源" },
|
||||
{ "file": "CorpusServer.java", "line": 26, "category": "process_record", "severity": "P1", "note": "无前缀提交溯源" },
|
||||
{ "file": "CorpusServer.java", "line": 29, "category": "process_record", "severity": "P1", "note": "阶段编号+交付范围叙述" },
|
||||
{ "file": "CorpusServer.java", "line": 32, "category": "process_record", "severity": "INFO", "note": "TODO 台账,日期视为截止信息随台账保留" },
|
||||
{ "file": "CorpusServer.java", "line": 35, "category": "process_record", "severity": "P1", "note": "数据范围约束+\"不再硬编码\"尾巴,拆分" },
|
||||
{ "file": "CorpusServer.java", "line": 38, "category": "process_record", "severity": "P1", "note": "字典项现状+\"不再限定旧的\"尾巴" },
|
||||
{ "file": "CorpusServer.java", "line": 41, "category": "process_record", "severity": "P1", "note": "\"六月原型\"过程引用+\"不再删除\"尾巴" },
|
||||
{ "file": "CorpusServer.java", "line": 44, "category": "process_record", "severity": "P1", "note": "框架能力事实+\"不再需要\"尾巴,改写为当前语义" },
|
||||
{ "file": "CorpusServer.java", "line": 47, "category": "normal_note", "severity": null, "note": "连接串示例,IP 不构成版本/日期信号" },
|
||||
{ "file": "CorpusUsage.sh", "line": 1, "category": "needs_verify", "severity": "P1", "note": "用法示例引用 ./ry.sh,疑似改名遗留但无法确认" },
|
||||
{ "file": "CorpusWorkflow.java", "line": 6, "category": "business_constraint", "severity": null, "note": "新旧版收文定义的字段语义均为业务规则" },
|
||||
{ "file": "CorpusWorkflow.java", "line": 9, "category": "business_constraint", "severity": null, "note": "新版定义=当前业务对象版本;\"后续节点\"为流程位置" },
|
||||
{ "file": "CorpusWorkflow.java", "line": 12, "category": "business_constraint", "severity": null, "note": "旧实例仍在流转=移除前提(业务事实)" },
|
||||
{ "file": "CorpusWorkflow.java", "line": 15, "category": "business_constraint", "severity": null, "note": "新版钉钉=外部产品版本" },
|
||||
{ "file": "CorpusWorkflow.java", "line": 18, "category": "business_constraint", "severity": null, "note": "测试用例标注旧版入口行为" },
|
||||
{ "file": "CorpusWorkflow.java", "line": 21, "category": "business_constraint", "severity": null, "note": "测试断言说明,旧版节点码为存量数据事实" },
|
||||
{ "file": "CorpusWorkflow.java", "line": 24, "category": "business_constraint", "severity": null, "note": "\"后续会签\"为流程先后位置,不是待办承诺" },
|
||||
{ "file": "api/other.ts", "line": 1, "category": "normal_note", "severity": null, "note": "支撑文件说明,无任何过程信号" },
|
||||
{ "file": "common/role-baseline.ts", "line": 1, "category": "needs_verify", "severity": "P1", "note": "本地测试配置不入库;双胞胎副本与自身同名,跨仓引用合法,不得据同名升为确认失效" }
|
||||
]
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# 接口约定
|
||||
|
||||
被移动的文档样例:api/contract.md 的引用应以本文件为近似候选。
|
||||
Reference in New Issue
Block a user