@ranger1/dx 0.1.104 → 0.1.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/codex-initial.js +79 -5
- package/package.json +1 -1
- package/skills/backend-audit-fixer/SKILL.md +98 -0
- package/skills/backend-audit-fixer/references/backend-layering.md +103 -0
- package/skills/backend-audit-fixer/references/e2e.md +60 -0
- package/skills/backend-audit-fixer/references/env-accessor.md +73 -0
- package/skills/backend-audit-fixer/references/error-handling.md +77 -0
- package/skills/{naming-audit-fixer/references/fix-guide.md → backend-audit-fixer/references/naming-fix-guide.md} +8 -3
- package/skills/backend-audit-fixer/references/naming.md +139 -0
- package/skills/backend-audit-fixer/references/pagination-dto.md +52 -0
- package/skills/create-issue/SKILL.md +90 -0
- package/skills/feature-decide-plan-execute/SKILL.md +537 -0
- package/skills/issues-batch-deliver/SKILL.md +549 -0
- package/skills/pr-train-ship/SKILL.md +669 -0
- package/skills/backend-layering-audit-fixer/SKILL.md +0 -180
- package/skills/e2e-audit-fixer/SKILL.md +0 -76
- package/skills/e2e-audit-fixer/agents/openai.yaml +0 -4
- package/skills/env-accessor-audit-fixer/SKILL.md +0 -149
- package/skills/env-accessor-audit-fixer/agents/openai.yaml +0 -7
- package/skills/error-handling-audit-fixer/SKILL.md +0 -187
- package/skills/error-handling-audit-fixer/agents/openai.yaml +0 -7
- package/skills/git-pr-ship/SKILL.md +0 -528
- package/skills/multi-pr-feature-delivery/SKILL.md +0 -493
- package/skills/multi-pr-feature-delivery/agents/openai.yaml +0 -7
- package/skills/naming-audit-fixer/SKILL.md +0 -149
- package/skills/pagination-dto-audit-fixer/SKILL.md +0 -69
- package/skills/pagination-dto-audit-fixer/agents/openai.yaml +0 -7
- /package/skills/{env-accessor-audit-fixer → backend-audit-fixer}/references/bootstrap-env-foundation.md +0 -0
- /package/skills/{error-handling-audit-fixer/references/foundation-bootstrap.md → backend-audit-fixer/references/error-handling-foundation-bootstrap.md} +0 -0
- /package/skills/{error-handling-audit-fixer → backend-audit-fixer}/references/error-handling-standard.md +0 -0
- /package/skills/{pagination-dto-audit-fixer → backend-audit-fixer}/references/pagination-standard.md +0 -0
- /package/skills/{e2e-audit-fixer/scripts/e2e_e2e_audit.py → backend-audit-fixer/scripts/e2e_audit.py} +0 -0
- /package/skills/{env-accessor-audit-fixer → backend-audit-fixer}/scripts/env_accessor_audit.py +0 -0
- /package/skills/{error-handling-audit-fixer → backend-audit-fixer}/scripts/error_handling_audit.py +0 -0
- /package/skills/{naming-audit-fixer/scripts/audit_naming.py → backend-audit-fixer/scripts/naming_audit.py} +0 -0
- /package/skills/{pagination-dto-audit-fixer → backend-audit-fixer}/scripts/pagination_dto_audit.py +0 -0
package/lib/codex-initial.js
CHANGED
|
@@ -6,6 +6,24 @@ import { logger } from './logger.js'
|
|
|
6
6
|
|
|
7
7
|
const TEMP_DIR_PATTERN = /^\..+\.(tmp|backup)-\d+-\d+$/
|
|
8
8
|
|
|
9
|
+
// 历史上曾在 skills/ 目录托管、但后续已删除的 skill 名称。
|
|
10
|
+
// 来源:git log --all --diff-filter=A --name-only -- 'skills/*' | sed -n 's#^skills/\([^/]*\)/.*#\1#p' | sort -u
|
|
11
|
+
// 再减去当前 skills/ 现存名单。
|
|
12
|
+
// 这些名字需要在 ~/.agents、~/.claude、~/.codex 三处彻底清理(软链或真实目录都清)。
|
|
13
|
+
// 将来从 skills/ 删除新的 skill 时,把它追加到这里即可。
|
|
14
|
+
const DELETED_SKILLS = [
|
|
15
|
+
'autospec',
|
|
16
|
+
'backend-layering-audit-fixer',
|
|
17
|
+
'e2e-audit-fixer',
|
|
18
|
+
'env-accessor-audit-fixer',
|
|
19
|
+
'error-handling-audit-fixer',
|
|
20
|
+
'multi-pr-feature-delivery',
|
|
21
|
+
'naming-convention-audit',
|
|
22
|
+
'omc-reference',
|
|
23
|
+
'pagination-dto-audit-fixer',
|
|
24
|
+
'pr-ship',
|
|
25
|
+
]
|
|
26
|
+
|
|
9
27
|
async function collectSkillNames(dir) {
|
|
10
28
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
11
29
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
|
@@ -51,6 +69,15 @@ async function pathExists(path) {
|
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
|
|
72
|
+
async function lstatIfExists(path) {
|
|
73
|
+
try {
|
|
74
|
+
return await fs.lstat(path)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error?.code === 'ENOENT') return null
|
|
77
|
+
throw error
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
async function assertDirExists(path, label) {
|
|
55
82
|
try {
|
|
56
83
|
const st = await fs.stat(path)
|
|
@@ -58,6 +85,9 @@ async function assertDirExists(path, label) {
|
|
|
58
85
|
throw new Error(`${label} 不是目录: ${path}`)
|
|
59
86
|
}
|
|
60
87
|
} catch (error) {
|
|
88
|
+
if (error?.message?.startsWith(`${label} 不是目录:`)) {
|
|
89
|
+
throw error
|
|
90
|
+
}
|
|
61
91
|
const message = error?.message || String(error)
|
|
62
92
|
throw new Error(`${label} 不存在或不可访问: ${path}\n${message}`)
|
|
63
93
|
}
|
|
@@ -113,18 +143,42 @@ async function copyDirMerge({ srcDir, dstDir }) {
|
|
|
113
143
|
}
|
|
114
144
|
|
|
115
145
|
async function removeManagedNonSymlinkSkills(skillsDir, skillNames) {
|
|
146
|
+
for (const skillName of skillNames) {
|
|
147
|
+
const target = join(skillsDir, skillName)
|
|
148
|
+
const stat = await lstatIfExists(target)
|
|
149
|
+
if (!stat) continue
|
|
150
|
+
if (stat.isSymbolicLink()) continue
|
|
151
|
+
await fs.rm(target, { recursive: true, force: true })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 彻底清理指定名称的 skill:无论是软链还是真实目录/文件都删除。
|
|
156
|
+
// 用于历史上已从 skills/ 删除的 skill,需在各目标目录连根拔除。
|
|
157
|
+
// 这是尽力而为的清理:单个名字清理失败(如权限问题)只记录并继续,
|
|
158
|
+
// 不抛出,避免一个陈旧副本的异常阻断整个 skill 同步主流程。
|
|
159
|
+
async function purgeSkills(skillsDir, skillNames) {
|
|
160
|
+
let purged = 0
|
|
161
|
+
let failed = 0
|
|
116
162
|
for (const skillName of skillNames) {
|
|
117
163
|
const target = join(skillsDir, skillName)
|
|
118
164
|
let stat
|
|
119
165
|
try {
|
|
120
|
-
stat = await
|
|
166
|
+
stat = await lstatIfExists(target)
|
|
121
167
|
} catch (error) {
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
logger.warn(`清理历史 skill 失败(跳过): ${target} — ${error?.message || String(error)}`)
|
|
169
|
+
failed++
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (!stat) continue
|
|
173
|
+
try {
|
|
174
|
+
await fs.rm(target, { recursive: true, force: true })
|
|
175
|
+
purged++
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logger.warn(`清理历史 skill 失败(跳过): ${target} — ${error?.message || String(error)}`)
|
|
178
|
+
failed++
|
|
124
179
|
}
|
|
125
|
-
if (stat.isSymbolicLink()) continue
|
|
126
|
-
await fs.rm(target, { recursive: true, force: true })
|
|
127
180
|
}
|
|
181
|
+
return { purged, failed }
|
|
128
182
|
}
|
|
129
183
|
|
|
130
184
|
async function removeStaleTempDirs(skillsDir) {
|
|
@@ -173,6 +227,12 @@ export async function runCodexInitial(options = {}) {
|
|
|
173
227
|
await assertDirExists(srcSkillsDir, '模板目录 skills')
|
|
174
228
|
const skillNames = await collectSkillNames(srcSkillsDir)
|
|
175
229
|
|
|
230
|
+
// 护栏:已删除名单不得与现存 skill 重叠,否则会把刚同步的 skill 又清掉(自相矛盾)。
|
|
231
|
+
const overlap = DELETED_SKILLS.filter((name) => skillNames.includes(name))
|
|
232
|
+
if (overlap.length > 0) {
|
|
233
|
+
throw new Error(`DELETED_SKILLS 与现存 skills/ 重叠,需修正名单: ${overlap.join(', ')}`)
|
|
234
|
+
}
|
|
235
|
+
|
|
176
236
|
await ensureDir(codexSkillsDir)
|
|
177
237
|
await ensureDir(claudeSkillsDir)
|
|
178
238
|
await ensureDir(agentsSkillsDir)
|
|
@@ -180,6 +240,13 @@ export async function runCodexInitial(options = {}) {
|
|
|
180
240
|
await removeManagedNonSymlinkSkills(codexSkillsDir, skillNames)
|
|
181
241
|
await removeManagedNonSymlinkSkills(claudeSkillsDir, skillNames)
|
|
182
242
|
|
|
243
|
+
// 清理历史上已删除的 skill:在 agents/claude/codex 三处连根拔除(含 agents 真实副本与软链)。
|
|
244
|
+
// 先清 claude(指向 agents 的软链宿主),再清 agents/codex(真实副本),
|
|
245
|
+
// 避免中途失败时留下指向已删除 agents target 的悬空软链。
|
|
246
|
+
const purgeClaude = await purgeSkills(claudeSkillsDir, DELETED_SKILLS)
|
|
247
|
+
const purgeAgents = await purgeSkills(agentsSkillsDir, DELETED_SKILLS)
|
|
248
|
+
const purgeCodex = await purgeSkills(codexSkillsDir, DELETED_SKILLS)
|
|
249
|
+
|
|
183
250
|
await removeStaleTempDirs(agentsSkillsDir)
|
|
184
251
|
const copyStats = await copyDirMerge({ srcDir: srcSkillsDir, dstDir: agentsSkillsDir })
|
|
185
252
|
await removeStaleTempDirs(agentsSkillsDir)
|
|
@@ -189,4 +256,11 @@ export async function runCodexInitial(options = {}) {
|
|
|
189
256
|
logger.info(`agents skills: 覆盖复制 ${copyStats.fileCount} 个文件 -> ${agentsSkillsDir}`)
|
|
190
257
|
logger.info(`claude skills: 已创建 ${skillNames.length} 个软链接 -> ${claudeSkillsDir}`)
|
|
191
258
|
logger.info(`codex skills: 已清理 ${skillNames.length} 个包内托管 skill 的旧副本 -> ${codexSkillsDir}`)
|
|
259
|
+
logger.info(
|
|
260
|
+
`已删除 skill 清理: agents ${purgeAgents.purged} / claude ${purgeClaude.purged} / codex ${purgeCodex.purged}`,
|
|
261
|
+
)
|
|
262
|
+
const purgeFailed = purgeClaude.failed + purgeAgents.failed + purgeCodex.failed
|
|
263
|
+
if (purgeFailed > 0) {
|
|
264
|
+
logger.warn(`已删除 skill 清理: ${purgeFailed} 个目标清理失败(已跳过,详见上方告警)`)
|
|
265
|
+
}
|
|
192
266
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-audit-fixer
|
|
3
|
+
description: 必须显式调用才触发
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 后端规范审计与修复(伞 skill)
|
|
7
|
+
|
|
8
|
+
## 概览
|
|
9
|
+
|
|
10
|
+
6 个后端审计维度的统一入口。规则太多,**一维度一 subagent,每次只跑一个**:用户每次选一个维度,派一个 subagent 只载那份 reference、只跑那个脚本,回来出该维度报告,再询问是否继续下一个。**不并行扇出、不一次跑全部。**
|
|
11
|
+
|
|
12
|
+
默认只审计出报告;用户明确说"修复/直接改"才进落代码阶段。
|
|
13
|
+
|
|
14
|
+
## 维度表
|
|
15
|
+
|
|
16
|
+
| 维度 | reference | 脚本 | 规则来源 |
|
|
17
|
+
|------|-----------|------|---------|
|
|
18
|
+
| backend-layering | references/backend-layering.md | 无(纯 rg) | conventions §4/§5/§6 |
|
|
19
|
+
| e2e | references/e2e.md | scripts/e2e_audit.py | ruler/e2e-audit.md |
|
|
20
|
+
| env-accessor | references/env-accessor.md | scripts/env_accessor_audit.py | conventions §2 |
|
|
21
|
+
| error-handling | references/error-handling.md | scripts/error_handling_audit.py | conventions §9 |
|
|
22
|
+
| naming | references/naming.md | scripts/naming_audit.py(需先拼 config) | conventions §10 |
|
|
23
|
+
| pagination-dto | references/pagination-dto.md | scripts/pagination_dto_audit.py | conventions §12 |
|
|
24
|
+
|
|
25
|
+
## 执行流程
|
|
26
|
+
|
|
27
|
+
### Step 1:询问要跑哪个维度
|
|
28
|
+
|
|
29
|
+
**每次只跑一个维度。** 进入 skill 后:
|
|
30
|
+
|
|
31
|
+
- 用户已点名某维度("检查命名"/"分页规范")→ 直接跑该维度,跳到 Step 2。
|
|
32
|
+
- 用户说"全量审计/扫一遍/检查合规"或没点名 → **用纯文本列出下面 6 个维度的编号菜单**,让用户回复编号或维度名再继续。别默认全跑、别并行。
|
|
33
|
+
|
|
34
|
+
> ⚠️ 不要用 AskUserQuestion 列维度:它每题最多 4 个选项,6 个维度会被截断。必须用文本菜单。
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
请选择要审计的维度(每次只跑一个,回复编号或名字):
|
|
38
|
+
1. backend-layering — 三层架构/事务/Repository 越层(conventions §4/§5/§6)
|
|
39
|
+
2. e2e — E2E 中文标题/手工 JWT/请求 helper/fixture 复用(ruler/e2e-audit.md)
|
|
40
|
+
3. env-accessor — 业务代码直读 process.env(conventions §2)
|
|
41
|
+
4. error-handling — 裸 BadRequestException/无 code HttpException(conventions §9)
|
|
42
|
+
5. naming — 文件/文件夹命名规范(conventions §10)
|
|
43
|
+
6. pagination-dto — 分页 DTO 未继承基类/手工拼装分页返回(conventions §12)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Step 2:派 1 个 subagent 跑选中的维度
|
|
47
|
+
|
|
48
|
+
只为这一个维度派一个 subagent。prompt 模板:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
你负责后端审计的「<维度名>」维度。
|
|
52
|
+
1. 只读 ~/.claude/skills/backend-audit-fixer/references/<维度>.md
|
|
53
|
+
2. 按其中说明运行该维度的脚本/命令(workspace=<绝对路径>)
|
|
54
|
+
3. 不要读其他维度的 reference,不要跑其他维度的脚本
|
|
55
|
+
4. 默认只审计,除非主任务明确要求修复
|
|
56
|
+
5. 严格按 reference 末尾的 findings JSON 契约返回结果(把脚本原始输出归一化成该契约)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
关键约定:
|
|
60
|
+
- 脚本路径统一 `SKILL_HOME="${SKILL_HOME:-$HOME/.claude/skills}"` → `$SKILL_HOME/backend-audit-fixer/scripts/<name>.py`。
|
|
61
|
+
- naming 维度特殊:subagent 要先分析项目拼 JSON config 再喂脚本(见 references/naming.md)。
|
|
62
|
+
- backend-layering 无脚本:subagent 跑 rg + 读代码判定。
|
|
63
|
+
|
|
64
|
+
### Step 3:出该维度报告
|
|
65
|
+
|
|
66
|
+
subagent 把脚本原始输出**归一化**成自己 reference 末尾的 findings 契约返回(脚本原始 JSON 顶层结构各异,契约是统一汇报格式,非脚本原样输出)。主 agent 据此出该维度报告:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
## <维度> 审计报告
|
|
70
|
+
- 基础设施:...
|
|
71
|
+
- 命中数:N
|
|
72
|
+
- 规则分布:...
|
|
73
|
+
|
|
74
|
+
### 详细列表
|
|
75
|
+
(展开 violations,标注脚本结果 vs 源码复核)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
区分清楚:脚本命中 vs 已抽样复核;src vs e2e(error-handling);疑似误报标"脚本规则限制"。
|
|
79
|
+
|
|
80
|
+
### Step 4:询问下一步
|
|
81
|
+
|
|
82
|
+
出完报告后,用 AskUserQuestion 问用户:
|
|
83
|
+
|
|
84
|
+
- 修复本维度(仅用户明确要求才落代码)
|
|
85
|
+
- 跑下一个维度(回 Step 1 再选一个)
|
|
86
|
+
- 结束
|
|
87
|
+
|
|
88
|
+
**不要出完一个维度就自动跑下一个。** 每个维度之间都要停下来等用户决定。
|
|
89
|
+
|
|
90
|
+
### 修复(仅用户明确要求)
|
|
91
|
+
|
|
92
|
+
按维度修复,照该维度 reference 的修复策略改码。修复后该维度复扫确认归零,再跑项目验证命令(`dx lint` / `dx build` / 受影响测试)。作者 pass 改码、复扫 pass 验证分两 lane,别自审自批。
|
|
93
|
+
|
|
94
|
+
## 注意
|
|
95
|
+
|
|
96
|
+
- **每次只跑一个维度**,维度之间用 AskUserQuestion 停顿,不并行、不自动连跑。
|
|
97
|
+
- 各 reference 自包含(触发/命令/规则/修复/排除/findings 契约),subagent 不需要回看本 SKILL.md。
|
|
98
|
+
- 旧的 6 个独立 skill(*-audit-fixer)过渡期并存;新工作走本伞 skill。
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# 维度:后端三层架构与事务规范(backend-layering)
|
|
2
|
+
|
|
3
|
+
无脚本维度。subagent 跑 rg + 读命中代码判定。规则来源:`ruler/conventions.md` §4 NestJS/Prisma + §5/§6 事务规范。
|
|
4
|
+
|
|
5
|
+
默认只输出审计结果与修复建议;用户明确要求时才自动修复。
|
|
6
|
+
|
|
7
|
+
## 检查项
|
|
8
|
+
|
|
9
|
+
### 分层架构
|
|
10
|
+
|
|
11
|
+
| 编号 | 违规类型 | 说明 |
|
|
12
|
+
|------|----------|------|
|
|
13
|
+
| L1 | Service 直接访问数据库 | Service 注入 `PrismaService` 并调用 `this.prisma.*` |
|
|
14
|
+
| L2 | Service 伪装为 Repository | Service 文件含 `getClient()` / `txHost.tx` 模式,实质是 Repository |
|
|
15
|
+
| L3 | Service 死依赖 | Service 注入 `PrismaService` 但从未使用 |
|
|
16
|
+
| L4 | Controller 跨层调用 | Controller 直接注入 Repository |
|
|
17
|
+
|
|
18
|
+
### 事务规范
|
|
19
|
+
|
|
20
|
+
| 编号 | 违规类型 | 说明 |
|
|
21
|
+
|------|----------|------|
|
|
22
|
+
| T1 | afterCommit 未被排空 | Controller 用 `@Transactional()` 但调用链中存在 `txEvents.afterCommit()` 回调(应改用 `@TransactionalWithAfterCommit()`) |
|
|
23
|
+
| T2 | Service 违规传播类型 | Service 使用 `Propagation.Required` / `RequiresNew` / `Nested`(禁止 Service 自行创建事务) |
|
|
24
|
+
| T3 | 直接 `prisma.$transaction()` | 绕过 `TransactionHost` 抽象 |
|
|
25
|
+
| T4 | SSE/流式端点加事务装饰器 | `@Sse()` 或流式返回方法上有事务装饰器(会导致 afterCommit 提前排空) |
|
|
26
|
+
| T5 | 非 HTTP 场景事务模式错误 | Subscriber/Scheduler 用 `@Transactional()` 而非 `txHost.withTransaction()` / `txEvents.withAfterCommit()` |
|
|
27
|
+
| T6 | 非 HTTP 场景缺少 CLS 作用域 | Scheduler 调 `txHost.withTransaction()` 前未用 `cls.run()` 创建 CLS 作用域 |
|
|
28
|
+
|
|
29
|
+
## 审计命令(并行执行)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# L1+L2+L3:Service 注入 PrismaService
|
|
33
|
+
rg "PrismaService" apps/backend/src/modules --glob '*.service.ts' -l
|
|
34
|
+
# L2:Service 含 Repository 模式
|
|
35
|
+
rg "getClient|txHost\.tx" apps/backend/src/modules --glob '*.service.ts' -l
|
|
36
|
+
# L4:Controller 注入 Repository
|
|
37
|
+
rg "Repository" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
38
|
+
# T1:只用 @Transactional() 的 Controller 方法
|
|
39
|
+
rg "@Transactional\(\)" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
40
|
+
# T2:Service 违规传播类型
|
|
41
|
+
rg "Propagation\.(Required|RequiresNew|Nested)" apps/backend/src/modules --glob '*.service.ts' -l
|
|
42
|
+
# T3:直接 prisma.$transaction()
|
|
43
|
+
rg "prisma\.\$transaction" apps/backend/src/modules -l
|
|
44
|
+
# T4:SSE 端点
|
|
45
|
+
rg "@Sse\(\)" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
46
|
+
# T5:Subscriber/Scheduler 误用 @Transactional
|
|
47
|
+
rg "@Transactional" apps/backend/src/modules --glob '*.subscriber.ts' --glob '*.task.ts' --glob '*.scheduler*.ts' -l
|
|
48
|
+
# T6:Scheduler 用 txHost.withTransaction 但未包 cls.run
|
|
49
|
+
rg "txHost\.withTransaction" apps/backend/src/modules --glob '*.scheduler*.ts' --glob '*.task.ts' -l
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 判定流程
|
|
53
|
+
|
|
54
|
+
1. 跑审计命令收集命中文件。
|
|
55
|
+
2. 分层:读构造函数+使用处判定:
|
|
56
|
+
- 注入 `PrismaService` 且有 `this.prisma.*` → **L1**
|
|
57
|
+
- 注入 `PrismaService`/`txHost` 且有 `getClient()` → **L2**
|
|
58
|
+
- 注入 `PrismaService` 但无任何 `this.prisma.*` → **L3**
|
|
59
|
+
- Controller import/注入 Repository → **L4**
|
|
60
|
+
3. 事务:
|
|
61
|
+
- T1:对只用 `@Transactional()` 的 Controller,追踪其调用的 Service 方法是否存在 `txEvents.afterCommit()`
|
|
62
|
+
- T2/T3/T5:直接匹配即违规
|
|
63
|
+
- T4:对 `@Sse()` 方法查同方法上是否有事务装饰器
|
|
64
|
+
- T6:查 `txHost.withTransaction()` 是否在 `cls.run()` 回调内部
|
|
65
|
+
4. 输出报告(按模块分组,标违规类型+行号)。
|
|
66
|
+
|
|
67
|
+
## 修复策略
|
|
68
|
+
|
|
69
|
+
- **L1**:DB 查询逻辑提取到 Repository,Service 改注入 Repository。
|
|
70
|
+
- **L2**:文件重命名 `*.repository.ts`,类名 `*Repository`,更新所有引用与 Module providers。
|
|
71
|
+
- **L3**:移除构造函数 `PrismaService` 注入及 import,跑 lint 确认无残留。
|
|
72
|
+
- **L4**:Repository 调用下沉到 Service,Controller 改调 Service。
|
|
73
|
+
- **T1**:`@Transactional()` → `@TransactionalWithAfterCommit()`(或确认无 afterCommit 则不改)。
|
|
74
|
+
- **T2**:改 `Propagation.Mandatory` 或 `Supports`,事务边界上移到 Controller。
|
|
75
|
+
- **T3**:改 `txHost.withTransaction()` 或由 Controller `@Transactional()` 声明边界。
|
|
76
|
+
- **T4**:移除装饰器;如需事务在流式逻辑内用 `cls.run()` + `txHost.withTransaction()` 局部处理。
|
|
77
|
+
- **T5**:Subscriber → `txEvents.withAfterCommit()`;Scheduler/Task → `cls.run()` + `txHost.withTransaction()`。
|
|
78
|
+
- **T6**:外层包 `cls.run()`:
|
|
79
|
+
```typescript
|
|
80
|
+
await this.cls.run(async () => {
|
|
81
|
+
await this.txHost.withTransaction(async () => { /* ... */ })
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 排除项
|
|
86
|
+
|
|
87
|
+
- `*.repository.ts` 使用 `PrismaService` / `getClient()` / `txHost`(Repository 正常职责)
|
|
88
|
+
- `prisma/` 基础设施文件(`prisma.service.ts` 等)
|
|
89
|
+
- `common/` 事务基础设施(`TransactionEventsService`、`AfterCommitInterceptor` 等)
|
|
90
|
+
- `*.spec.ts` / `e2e/` 测试文件
|
|
91
|
+
- Subscriber/Scheduler 中通过 `txHost.withTransaction()` 管理事务(正确模式)
|
|
92
|
+
|
|
93
|
+
## 返回给主 agent 的 findings
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"dimension": "backend-layering",
|
|
98
|
+
"infra_status": "n/a",
|
|
99
|
+
"total": 0,
|
|
100
|
+
"by_rule": {"L1":0,"L2":0,"L3":0,"L4":0,"T1":0,"T2":0,"T3":0,"T4":0,"T5":0,"T6":0},
|
|
101
|
+
"violations": [{"file":"","rule":"L1","line":0,"note":""}]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# 维度:E2E 测试可维护性(e2e)
|
|
2
|
+
|
|
3
|
+
脚本:`scripts/e2e_audit.py`。规则来源:`ruler/e2e-audit.md`。
|
|
4
|
+
|
|
5
|
+
默认只检测+插 TODO 注释,不动业务逻辑;自动修复仅对中文测试名做真实替换。
|
|
6
|
+
|
|
7
|
+
## 触发场景
|
|
8
|
+
|
|
9
|
+
- 检查 `apps/backend/e2e/**/*.e2e-spec.ts` 用例是否符合英文命名。
|
|
10
|
+
- 识别直接操作 Prisma、手工 JWT、手工 API URL、手工请求实现,区分哪些改全局 fixture、哪些抽本地 helper。
|
|
11
|
+
|
|
12
|
+
## 运行
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
SKILL_HOME="${SKILL_HOME:-$HOME/.claude/skills}"
|
|
16
|
+
python "$SKILL_HOME/backend-audit-fixer/scripts/e2e_audit.py" \
|
|
17
|
+
--workspace /Users/a1/work/ai-monorepo \
|
|
18
|
+
--output-json /tmp/audit-e2e.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
路径参数(全部可覆盖,禁止硬编码):
|
|
22
|
+
- `--workspace`:代码根目录,默认当前目录。
|
|
23
|
+
- `--e2e-glob`:扫描模式,默认 `apps/backend/e2e/**/*.e2e-spec.ts`。
|
|
24
|
+
- `--fixtures`:fixtures 路径,默认 `${workspace}/apps/backend/e2e/fixtures/fixtures.ts`。
|
|
25
|
+
|
|
26
|
+
## 检查规则
|
|
27
|
+
|
|
28
|
+
- `e2e-chinese`:`describe/it/test/context` 名称含中文字符。
|
|
29
|
+
- `e2e-fixtures`:`prisma.user.*`、`prisma.userCredential.*`、`jwtService.sign/jwt.sign`、未用 `buildApiUrl()` 的 API URL 片段、未用 `createAuthRequest/createAdminAuthRequest/createPublicRequest` 的手工请求。
|
|
30
|
+
- `e2e-local-helper`:其他 `prisma.*.create*` / `upsert` 重复实现,默认只建议当前文件内抽本地 helper,不上收全局 fixtures。
|
|
31
|
+
|
|
32
|
+
## 修复策略
|
|
33
|
+
|
|
34
|
+
1. 中文测试名:先翻译英文。可提供翻译映射后 `--apply`:
|
|
35
|
+
```bash
|
|
36
|
+
python "$SKILL_HOME/backend-audit-fixer/scripts/e2e_audit.py" \
|
|
37
|
+
--workspace /Users/a1/work/ai-monorepo \
|
|
38
|
+
--translation-map /tmp/name-map.json --apply
|
|
39
|
+
```
|
|
40
|
+
翻译三选一:`--translation-map`(JSON `{"中文":"English"}`,推荐)/ `--translate-service openai`(需 `OPENAI_API_KEY`)/ 不提供(仅输出不改写)。
|
|
41
|
+
2. fixture 重复实现:自动加注释+替换建议,保留原逻辑。仅 `user`/`userCredential` 为全局 fixture 候选,其余建议本地 helper。
|
|
42
|
+
3. 复扫确认无回归。
|
|
43
|
+
|
|
44
|
+
## 注意
|
|
45
|
+
|
|
46
|
+
- 扫描不改业务逻辑,只检测+可控注释插入。
|
|
47
|
+
- fixture/请求构造类问题仅插 TODO 注释,不宣称已重构。
|
|
48
|
+
- 翻译服务仅处理测试名称。
|
|
49
|
+
|
|
50
|
+
## 返回给主 agent 的 findings
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"dimension": "e2e",
|
|
55
|
+
"total": 0,
|
|
56
|
+
"by_rule": {"e2e-chinese":0,"e2e-fixtures":0,"e2e-local-helper":0},
|
|
57
|
+
"violations": [{"file":"","rule":"e2e-chinese","line":0,"note":""}]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
通过标准:`count: 0` / `by_type: {}`。
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 维度:环境变量访问(env-accessor)
|
|
2
|
+
|
|
3
|
+
脚本:`scripts/env_accessor_audit.py`。规则来源:`ruler/conventions.md` §2 环境变量访问。
|
|
4
|
+
|
|
5
|
+
先扫 `process.env` 直读点,再判断是否已有统一 env 基础设施;有则复用迁移,缺则先补最小基础设施再收口。
|
|
6
|
+
|
|
7
|
+
## 运行
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
SKILL_HOME="${SKILL_HOME:-$HOME/.claude/skills}"
|
|
11
|
+
python "$SKILL_HOME/backend-audit-fixer/scripts/env_accessor_audit.py" \
|
|
12
|
+
--workspace /Users/a1/work/ai-monorepo \
|
|
13
|
+
--output-json /tmp/audit-env.json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`rg` 快速复核:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
rg "process\.env" apps/backend/src apps/backend/e2e \
|
|
20
|
+
--glob '!*env.accessor.ts' --glob '!*env.service.ts'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 执行流程
|
|
24
|
+
|
|
25
|
+
1. 扫 `apps/backend/src` 与 `apps/backend/e2e` 的 `process.env`。
|
|
26
|
+
2. 仅排除真正封装文件:`env.accessor.ts`、`env.service.ts`。**不排除** `apps/backend/src/config/**`(`registerAs` 配置层也纳入审计)。
|
|
27
|
+
3. 识别基础设施状态:是否存在 `createEnvAccessor` / `defaultEnvAccessor` / `EnvService` / `EnvModule` / `registerAs` 经统一入口读取。
|
|
28
|
+
4. 按场景为每个直读点选迁移方式,不机械替换。
|
|
29
|
+
5. 修复后复扫 + 补跑最小验证。
|
|
30
|
+
|
|
31
|
+
## 修复准则
|
|
32
|
+
|
|
33
|
+
- **配置层与 `registerAs`**:优先 `defaultEnvAccessor`;必须显式传环境对象时用 `createEnvAccessor(process.env)`;不再裸读。
|
|
34
|
+
```typescript
|
|
35
|
+
import { registerAs } from '@nestjs/config'
|
|
36
|
+
import { defaultEnvAccessor } from '@/common/env/env.accessor'
|
|
37
|
+
const env = defaultEnvAccessor
|
|
38
|
+
export const redisConfig = registerAs('redis', () => ({
|
|
39
|
+
host: env.str('REDIS_HOST', 'localhost'),
|
|
40
|
+
}))
|
|
41
|
+
```
|
|
42
|
+
- **运行期服务/控制器/提供者**:注入 `EnvService`,用 `getString/getInt/getBoolean/isProd/isE2E` 等 typed getter;模块未暴露 `EnvModule` 先补依赖。
|
|
43
|
+
- **独立脚本/CLI**:dotenv 装载后显式 `const env = createEnvAccessor(process.env)`。
|
|
44
|
+
- **必须读原始值**:用 `EnvService.getAccessor().raw(key)` 或 accessor 的 `raw(key)`,并注释说明为何 typed getter 不适用。
|
|
45
|
+
|
|
46
|
+
## 缺失基础设施时的补齐顺序
|
|
47
|
+
|
|
48
|
+
见 [bootstrap-env-foundation.md](./bootstrap-env-foundation.md)。顺序:
|
|
49
|
+
1. `env.accessor.ts`(`createEnvAccessor`/`defaultEnvAccessor`,支持 `str/bool/int/num/raw/appEnv/snapshot`)
|
|
50
|
+
2. `env.service.ts`(包 `ConfigService` + typed getter)
|
|
51
|
+
3. `env.module.ts`(暴露 `EnvService`)
|
|
52
|
+
4. 配置层迁 `registerAs + defaultEnvAccessor`
|
|
53
|
+
5. 运行期服务迁 `EnvService`
|
|
54
|
+
6. 复扫剩余直读点
|
|
55
|
+
|
|
56
|
+
优先复用现有命名/目录/模块结构。
|
|
57
|
+
|
|
58
|
+
## 例外
|
|
59
|
+
|
|
60
|
+
- `env.accessor.ts`/`env.service.ts` 本身允许访问 `process.env`。
|
|
61
|
+
- 负责 dotenv 装载、环境注入、测试临时覆写的底层入口可保留少量受控访问。
|
|
62
|
+
- 测试里显式设值视为受控例外,但优先复用公共 fixture/helper。
|
|
63
|
+
|
|
64
|
+
## 返回给主 agent 的 findings
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"dimension": "env-accessor",
|
|
69
|
+
"infra_status": {"createEnvAccessor":false,"defaultEnvAccessor":false,"EnvService":false,"EnvModule":false},
|
|
70
|
+
"total": 0,
|
|
71
|
+
"violations": [{"file":"","line":0,"suggestion":""}]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# 维度:错误处理(error-handling)
|
|
2
|
+
|
|
3
|
+
脚本:`scripts/error_handling_audit.py`。规则来源:`ruler/conventions.md` §9 统一错误码。
|
|
4
|
+
|
|
5
|
+
先判断是否已具备统一错误处理基础设施,再扫业务代码是否绕过 `DomainException` / `ErrorCode`。默认只输出审计+建议,用户明确要求才自动修复。
|
|
6
|
+
|
|
7
|
+
## 扫描范围
|
|
8
|
+
|
|
9
|
+
脚本不预设路径,需 `--src-dir` / `--e2e-dir` 显式传入。以下始终排除:
|
|
10
|
+
单测 `*.spec.ts`/`*.test.ts`/`*.e2e-spec.ts`;测试辅助 `*.mock.ts`/`*.stub.ts`/`*.fixture.ts`/`fixtures/`/`test-utils/`/`testing/`/`__tests__/`;基础设施 `*/filters/`/`prisma/`/`scripts/`;异常定义 `*.exception.ts`;入口 `main.ts`。
|
|
11
|
+
|
|
12
|
+
仅用户**明确要求**评估测试债务时才传 `--e2e-dir` + `--scope e2e`。
|
|
13
|
+
|
|
14
|
+
## 运行
|
|
15
|
+
|
|
16
|
+
步骤 0 先探索项目(识别生产代码与测试目录,monorepo 多服务各传一个 `--src-dir`)。
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
SKILL_HOME="${SKILL_HOME:-$HOME/.claude/skills}"
|
|
20
|
+
python "$SKILL_HOME/backend-audit-fixer/scripts/error_handling_audit.py" \
|
|
21
|
+
--workspace "$PWD" \
|
|
22
|
+
--src-dir apps/backend/src \
|
|
23
|
+
--output-json /tmp/audit-error.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
仅在用户要求时审计测试代码:`--e2e-dir apps/backend/e2e --scope e2e`。
|
|
27
|
+
|
|
28
|
+
## 识别四类问题
|
|
29
|
+
|
|
30
|
+
1. 直接实例化 Nest 标准异常(`BadRequestException`、`HttpException` 等)。
|
|
31
|
+
2. `throw new Error(...)` 或 `Promise.reject(new Error(...))`。
|
|
32
|
+
3. 直接 `new DomainException(...)` 但 payload 未显式带 `code`。
|
|
33
|
+
4. `DomainException` 直接返回中文 message。
|
|
34
|
+
|
|
35
|
+
## 合法 raw-error 判定(强制复核,别原样转发脚本结果)
|
|
36
|
+
|
|
37
|
+
脚本 `raw-error` 规则误报率极高(实测一次 18 命中,复核后 17 个是误报)。**每条 `raw-error` 命中必须打开源码定位,按下表判定 `real` 还是 `false-positive`,禁止把脚本命中直接当 violation 报、禁止给误报写"建议改 XXX"。** 下列模式属合法 raw-error,不算违规:
|
|
38
|
+
|
|
39
|
+
| 合法模式 | 识别特征 | 实例 |
|
|
40
|
+
|---------|---------|------|
|
|
41
|
+
| 本地 catch 控制流 | `try` 内 `throw new Error` 紧接着被**同函数本地 `catch`** 捕获(吞掉 / 返回 null / 重试下一候选),不向 HTTP 上抛 | metadata 解析校验后 `catch { return null }`;候选循环 `lastError` 重试 |
|
|
42
|
+
| 内部不变量断言 | programmer invariant / 生命周期断言,触发即代码 bug,非用户可达业务错误 | `called before onModuleInit`、`Character missing in RequestContext`、`rows must match delete window`、启动期常量自检 |
|
|
43
|
+
| 运维脚本/非 HTTP 路径 | backfill、dry-run、CLI、Cron 等非 HTTP 业务请求路径的参数/SOP 校验 | `backfill window exceeds max`、`refuse to overwrite missing row` |
|
|
44
|
+
| 控制流信号 | `Promise.race` 超时、迭代终止等把 Error 当信号用,非异常上抛 | `setTimeout(() => reject(new Error('timeout')))` |
|
|
45
|
+
|
|
46
|
+
真违规特征:**走 HTTP 业务请求路径(Controller→Service→Repository)、资源不存在 / 参数非法 / 状态冲突等用户可达错误、错误会冒泡到响应**。这类才报 `real` 并建议领域异常。
|
|
47
|
+
|
|
48
|
+
`domain-exception-missing-code` 同样要复核:若 `code` 是变量(如 `classify400()` 返回值)赋给 payload,脚本静态匹配不到但实际带了 code,属误报。
|
|
49
|
+
|
|
50
|
+
## 修复准则(优先级固定)
|
|
51
|
+
|
|
52
|
+
1. **优先复用现有领域异常**:模块 `exceptions/` 已有语义匹配类直接复用。
|
|
53
|
+
2. **缺少则新增领域异常类**:在模块 `exceptions/` 下继承 `DomainException`,构造函数显式指定 `ErrorCode`,上下文放 `args`(不写死文案到 message),补最小单测。
|
|
54
|
+
3. **临时直接用 DomainException**:仅在未抽专用类但必须推进时;payload 必含 `code`,`args` 保留排障上下文。
|
|
55
|
+
4. **基础设施缺失判断**:无 `DomainException`/`ErrorCode` → 先补基础设施,别发散新增几十个本地异常;有 `DomainException` 无统一 `ErrorCode` → 先统一错误码源;有类有码缺结构化输出链路 → 先补过滤器/输出映射,保证 `code`/`args`/`requestId` 稳定透出。
|
|
56
|
+
|
|
57
|
+
详见 [error-handling-standard.md](./error-handling-standard.md) 与 [error-handling-foundation-bootstrap.md](./error-handling-foundation-bootstrap.md)。
|
|
58
|
+
|
|
59
|
+
## 注意
|
|
60
|
+
|
|
61
|
+
- 明确区分 `src` 与 `e2e` 命中数量,别让测试噪音淹没生产风险。
|
|
62
|
+
- 脚本结果与实际不一致时,抽样打开命中文件复核,别当绝对真相。
|
|
63
|
+
- **每条命中必带 `verdict`**:`real`(已开源码确认走 HTTP 业务路径、用户可达错误)或 `false-positive`(命中"合法 raw-error 判定"表任一模式,`note` 写明哪类)。未开源码确认不准报 `real`。
|
|
64
|
+
|
|
65
|
+
## 返回给主 agent 的 findings
|
|
66
|
+
|
|
67
|
+
`total` 是脚本原始命中数;`real_total` 是复核后真违规数。两者都要给,让主 agent 看到误报比例。
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"dimension": "error-handling",
|
|
72
|
+
"infra_status": {"DomainException":false,"ErrorCode":false,"filter":false},
|
|
73
|
+
"src": {"total":0,"real_total":0,"by_rule":{}},
|
|
74
|
+
"e2e": {"total":0,"real_total":0,"by_rule":{}},
|
|
75
|
+
"violations": [{"file":"","scope":"src","rule":"","line":0,"verdict":"real|false-positive","note":"real 写建议领域异常;false-positive 写命中哪类合法模式"}]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
@@ -78,14 +78,19 @@ dx build admin --dev
|
|
|
78
78
|
| Hook .ts | camelCase | `useChat.ts` |
|
|
79
79
|
| 路由文件 | 小写 | `page.tsx`, `layout.tsx` |
|
|
80
80
|
| 目录 | kebab-case | `character/`(单数) |
|
|
81
|
-
| ui/ | kebab-case | `button.tsx` |
|
|
81
|
+
| ui/ | kebab-case(shadcn 原生,**豁免 PascalCase**) | `button.tsx` |
|
|
82
|
+
| 点号描述符 | 保留,仅 base 段 kebab | `announcement-popup.storage.ts` ✅ |
|
|
82
83
|
|
|
83
|
-
### Vite + React
|
|
84
|
+
### Vite + React(admin-front)
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
组件/工具命名同 Next.js。但 **`pages/**` 走 vite-plugin-pages 文件路由**:文件名即 URL,`.tsx` 改名属破坏性,默认不动(见高风险注意)。
|
|
86
87
|
|
|
87
88
|
## 高风险操作注意
|
|
88
89
|
|
|
90
|
+
- **vite-plugin-pages 文件路由(admin-front 最高危)**:`pages/**` 文件名即 URL。改名前 `rg "vite-plugin-pages|react-pages"`;命中则 `pages/**/*.tsx` 默认不改(改名 = 改 URL,且常无 import 纯靠路由存活,改完直接失联)。确需改:同步改 Pages `exclude` 或确认 URL 可变更并手测路由可达。
|
|
91
|
+
- **点号是描述符不是违规**:§10 认可 `.storage`/`.toggle`/`.core` 等描述符点号。改名**只动 base 段大小写**,保留点号(`CharactersPane.sub-normalize.ts`→`characters-pane.sub-normalize.ts`,不要拍平成 `characters-pane-sub-normalize.ts`)。
|
|
92
|
+
- **macOS 大小写不敏感**:`git mv MessageCenter message-center` 仅改大小写可能失败,需中间名过渡(`MessageCenter`→`message-center-tmp`→`message-center`)。
|
|
93
|
+
- **深引用 vs barrel**:改目录名前先 `rg` 确认外部走深路径还是 barrel——别假设走 index.ts,本仓实测外部全是 `Dir/Component` 深引用、barrel 零外部消费。
|
|
89
94
|
- **barrel export (index.ts)**:重命名后检查是否有 `export * from './old-name'`
|
|
90
95
|
- **动态 import**:`import()` 表达式中的路径也需要更新
|
|
91
96
|
- **tsconfig paths**:如果 tsconfig 中配置了路径别名指向具体文件,也需更新
|