@ranger1/dx 0.1.91 → 0.1.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/cli/help.js +1 -1
- package/lib/codex-initial.js +24 -211
- package/package.json +2 -2
- package/skills/backend-layering-audit-fixer/SKILL.md +180 -0
- package/{codex/skills → skills}/doctor/SKILL.md +2 -9
- package/{codex/skills → skills}/doctor/scripts/doctor.sh +2 -253
- package/skills/git-pr-ship/SKILL.md +481 -0
- package/skills/naming-audit-fixer/SKILL.md +149 -0
- package/skills/naming-audit-fixer/references/fix-guide.md +93 -0
- package/skills/naming-audit-fixer/scripts/audit_naming.py +534 -0
- package/codex/agents/fixer.toml +0 -37
- package/codex/agents/orchestrator.toml +0 -11
- package/codex/agents/reviewer.toml +0 -52
- package/codex/agents/spark.toml +0 -18
- package/codex/skills/pr-review-loop/SKILL.md +0 -209
- package/codex/skills/pr-review-loop/agents/openai.yaml +0 -4
- package/codex/skills/pr-review-loop/references/agents/pr-context.md +0 -73
- package/codex/skills/pr-review-loop/references/agents/pr-precheck.md +0 -161
- package/codex/skills/pr-review-loop/references/agents/pr-review-aggregate.md +0 -188
- package/codex/skills/pr-review-loop/references/skill-layout.md +0 -25
- package/codex/skills/pr-review-loop/scripts/gh_review_harvest.py +0 -292
- package/codex/skills/pr-review-loop/scripts/pr_context.py +0 -351
- package/codex/skills/pr-review-loop/scripts/pr_review_aggregate.py +0 -951
- package/codex/skills/pr-review-loop/scripts/test_pr_review_aggregate.py +0 -876
- package/codex/skills/pr-review-loop/scripts/test_validate_reviewer_prompts.py +0 -92
- package/codex/skills/pr-review-loop/scripts/validate_reviewer_prompts.py +0 -87
- /package/{codex/skills → skills}/doctor/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/scripts/e2e_e2e_audit.py +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/references/bootstrap-env-foundation.md +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/scripts/env_accessor_audit.py +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/references/error-handling-standard.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/references/foundation-bootstrap.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/scripts/error_handling_audit.py +0 -0
- /package/{codex/skills → skills}/gh-dependabot-cleanup/SKILL.md +0 -0
- /package/{codex/skills → skills}/gh-dependabot-cleanup/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/git-commit-and-pr/SKILL.md +0 -0
- /package/{codex/skills → skills}/git-commit-and-pr/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/git-release/SKILL.md +0 -0
- /package/{codex/skills → skills}/git-release/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/online-debug-guard/SKILL.md +0 -0
- /package/{codex/skills → skills}/online-debug-guard/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/references/pagination-standard.md +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/scripts/pagination_dto_audit.py +0 -0
package/README.md
CHANGED
|
@@ -148,8 +148,8 @@ target(端)不写死,由 `env-policy.jsonc.targets` 定义;`commands.jso
|
|
|
148
148
|
|
|
149
149
|
技能入口与说明见:
|
|
150
150
|
|
|
151
|
-
- `
|
|
152
|
-
- `
|
|
151
|
+
- `skills/pr-review-loop/SKILL.md`
|
|
152
|
+
- `skills/pr-review-loop/references/agents/*.md`
|
|
153
153
|
|
|
154
154
|
### 工作流概览
|
|
155
155
|
|
package/lib/cli/help.js
CHANGED
package/lib/codex-initial.js
CHANGED
|
@@ -4,48 +4,7 @@ import os from 'node:os'
|
|
|
4
4
|
|
|
5
5
|
import { logger } from './logger.js'
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
{
|
|
9
|
-
section: 'features',
|
|
10
|
-
values: {
|
|
11
|
-
multi_agent: 'true',
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
section: 'agents',
|
|
16
|
-
values: {
|
|
17
|
-
max_threads: '15',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
section: 'agents.fixer',
|
|
22
|
-
values: {
|
|
23
|
-
description: '"bugfix 代理"',
|
|
24
|
-
config_file: '"agents/fixer.toml"',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
section: 'agents.orchestrator',
|
|
29
|
-
values: {
|
|
30
|
-
description: '"pr 修复流程编排代理"',
|
|
31
|
-
config_file: '"agents/orchestrator.toml"',
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
section: 'agents.reviewer',
|
|
36
|
-
values: {
|
|
37
|
-
description: '"代码评审代理"',
|
|
38
|
-
config_file: '"agents/reviewer.toml"',
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
section: 'agents.spark',
|
|
43
|
-
values: {
|
|
44
|
-
description: '"通用执行代理"',
|
|
45
|
-
config_file: '"agents/spark.toml"',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
]
|
|
7
|
+
const DEPRECATED_SKILL_DIRS = ['pr-review-loop', 'git-commit-and-pr']
|
|
49
8
|
|
|
50
9
|
async function collectAllFiles(dir) {
|
|
51
10
|
const out = []
|
|
@@ -68,6 +27,7 @@ async function collectAllFiles(dir) {
|
|
|
68
27
|
continue
|
|
69
28
|
}
|
|
70
29
|
if (!entry.isFile()) continue
|
|
30
|
+
|
|
71
31
|
const lowerName = entry.name.toLowerCase()
|
|
72
32
|
if (lowerName.endsWith('.pyc') || lowerName.endsWith('.pyo') || lowerName.endsWith('.pyd')) continue
|
|
73
33
|
out.push(full)
|
|
@@ -82,110 +42,6 @@ async function ensureDir(path) {
|
|
|
82
42
|
await fs.mkdir(path, { recursive: true })
|
|
83
43
|
}
|
|
84
44
|
|
|
85
|
-
function escapeRegExp(input) {
|
|
86
|
-
return String(input).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function ensureTrailingNewline(text) {
|
|
90
|
-
if (!text) return ''
|
|
91
|
-
return text.endsWith('\n') ? text : `${text}\n`
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function upsertTomlSection(content, { section, values }) {
|
|
95
|
-
const header = `[${section}]`
|
|
96
|
-
const sectionPattern = new RegExp(`^\\[${escapeRegExp(section)}\\]\\s*$`, 'm')
|
|
97
|
-
const sectionHeaderMatch = content.match(sectionPattern)
|
|
98
|
-
let nextContent = content
|
|
99
|
-
let changedKeys = 0
|
|
100
|
-
let createdSection = false
|
|
101
|
-
|
|
102
|
-
if (!sectionHeaderMatch) {
|
|
103
|
-
const blockLines = [header, ...Object.entries(values).map(([key, value]) => `${key} = ${value}`), '']
|
|
104
|
-
nextContent = ensureTrailingNewline(content)
|
|
105
|
-
if (nextContent.length > 0 && !nextContent.endsWith('\n\n')) {
|
|
106
|
-
nextContent += '\n'
|
|
107
|
-
}
|
|
108
|
-
nextContent += `${blockLines.join('\n')}\n`
|
|
109
|
-
return {
|
|
110
|
-
content: nextContent,
|
|
111
|
-
changedKeys: Object.keys(values).length,
|
|
112
|
-
createdSection: true,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const sectionStart = sectionHeaderMatch.index
|
|
117
|
-
const sectionBodyStart = sectionStart + sectionHeaderMatch[0].length
|
|
118
|
-
const remaining = content.slice(sectionBodyStart)
|
|
119
|
-
const nextHeaderMatch = remaining.match(/\n(?=\[[^\]]+\]\s*$)/m)
|
|
120
|
-
const sectionEnd =
|
|
121
|
-
nextHeaderMatch && typeof nextHeaderMatch.index === 'number'
|
|
122
|
-
? sectionBodyStart + nextHeaderMatch.index + 1
|
|
123
|
-
: content.length
|
|
124
|
-
|
|
125
|
-
const beforeSection = content.slice(0, sectionStart)
|
|
126
|
-
const originalSectionText = content.slice(sectionStart, sectionEnd)
|
|
127
|
-
const trailing = content.slice(sectionEnd)
|
|
128
|
-
const sectionLines = originalSectionText.split('\n')
|
|
129
|
-
|
|
130
|
-
for (const [key, value] of Object.entries(values)) {
|
|
131
|
-
const desiredLine = `${key} = ${value}`
|
|
132
|
-
const keyPattern = new RegExp(`^${escapeRegExp(key)}\\s*=`, 'm')
|
|
133
|
-
const lineIndex = sectionLines.findIndex(line => keyPattern.test(line.trim()))
|
|
134
|
-
|
|
135
|
-
if (lineIndex === -1) {
|
|
136
|
-
let insertIndex = sectionLines.length
|
|
137
|
-
while (insertIndex > 1 && sectionLines[insertIndex - 1] === '') {
|
|
138
|
-
insertIndex--
|
|
139
|
-
}
|
|
140
|
-
sectionLines.splice(insertIndex, 0, desiredLine)
|
|
141
|
-
changedKeys++
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (sectionLines[lineIndex].trim() !== desiredLine) {
|
|
146
|
-
sectionLines[lineIndex] = desiredLine
|
|
147
|
-
changedKeys++
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const updatedSectionText = ensureTrailingNewline(sectionLines.join('\n'))
|
|
152
|
-
nextContent = `${beforeSection}${updatedSectionText}${trailing}`
|
|
153
|
-
|
|
154
|
-
return { content: nextContent, changedKeys, createdSection }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function ensureCodexConfig({ codexDir }) {
|
|
158
|
-
const configPath = join(codexDir, 'config.toml')
|
|
159
|
-
let content = ''
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
content = await fs.readFile(configPath, 'utf8')
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (error?.code !== 'ENOENT') throw error
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let changedKeys = 0
|
|
168
|
-
let createdSections = 0
|
|
169
|
-
let nextContent = content
|
|
170
|
-
|
|
171
|
-
for (const sectionConfig of REQUIRED_CODEX_CONFIG) {
|
|
172
|
-
const result = upsertTomlSection(nextContent, sectionConfig)
|
|
173
|
-
nextContent = result.content
|
|
174
|
-
changedKeys += result.changedKeys
|
|
175
|
-
if (result.createdSection) createdSections++
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (nextContent !== content || content === '') {
|
|
179
|
-
await fs.writeFile(configPath, ensureTrailingNewline(nextContent), 'utf8')
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
configPath,
|
|
184
|
-
changedKeys,
|
|
185
|
-
createdSections,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
45
|
async function assertDirExists(path, label) {
|
|
190
46
|
try {
|
|
191
47
|
const st = await fs.stat(path)
|
|
@@ -198,35 +54,6 @@ async function assertDirExists(path, label) {
|
|
|
198
54
|
}
|
|
199
55
|
}
|
|
200
56
|
|
|
201
|
-
async function copyTemplateTree({ srcDir, dstDir }) {
|
|
202
|
-
const files = await collectTemplateFiles(srcDir)
|
|
203
|
-
let mdCount = 0
|
|
204
|
-
let pyCount = 0
|
|
205
|
-
let jsonCount = 0
|
|
206
|
-
|
|
207
|
-
for (const file of files) {
|
|
208
|
-
const rel = relative(srcDir, file)
|
|
209
|
-
const target = join(dstDir, rel)
|
|
210
|
-
await ensureDir(dirname(target))
|
|
211
|
-
await fs.copyFile(file, target)
|
|
212
|
-
|
|
213
|
-
if (file.endsWith('.md')) {
|
|
214
|
-
mdCount++
|
|
215
|
-
} else if (file.endsWith('.py')) {
|
|
216
|
-
pyCount++
|
|
217
|
-
try {
|
|
218
|
-
await fs.chmod(target, 0o755)
|
|
219
|
-
} catch {
|
|
220
|
-
// 忽略权限设置失败
|
|
221
|
-
}
|
|
222
|
-
} else if (file.endsWith('.json')) {
|
|
223
|
-
jsonCount++
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return { total: files.length, md: mdCount, py: pyCount, json: jsonCount }
|
|
228
|
-
}
|
|
229
|
-
|
|
230
57
|
async function copyDirMerge({ srcDir, dstDir }) {
|
|
231
58
|
const files = await collectAllFiles(srcDir)
|
|
232
59
|
|
|
@@ -240,22 +67,10 @@ async function copyDirMerge({ srcDir, dstDir }) {
|
|
|
240
67
|
return { fileCount: files.length }
|
|
241
68
|
}
|
|
242
69
|
|
|
243
|
-
async function
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
let copiedFiles = 0
|
|
247
|
-
|
|
248
|
-
for (const entry of entries) {
|
|
249
|
-
if (!entry.isDirectory()) continue
|
|
250
|
-
const srcDir = join(srcSkillsDir, entry.name)
|
|
251
|
-
const dstDir = join(dstSkillsDir, entry.name)
|
|
252
|
-
await ensureDir(dstDir)
|
|
253
|
-
const stats = await copyDirMerge({ srcDir, dstDir })
|
|
254
|
-
copiedDirs++
|
|
255
|
-
copiedFiles += stats.fileCount
|
|
70
|
+
async function removeDeprecatedSkillDirs(skillsDir) {
|
|
71
|
+
for (const dirName of DEPRECATED_SKILL_DIRS) {
|
|
72
|
+
await fs.rm(join(skillsDir, dirName), { recursive: true, force: true })
|
|
256
73
|
}
|
|
257
|
-
|
|
258
|
-
return { copiedDirs, copiedFiles }
|
|
259
74
|
}
|
|
260
75
|
|
|
261
76
|
export async function runCodexInitial(options = {}) {
|
|
@@ -263,26 +78,24 @@ export async function runCodexInitial(options = {}) {
|
|
|
263
78
|
if (!packageRoot) throw new Error('runCodexInitial: 缺少 packageRoot')
|
|
264
79
|
|
|
265
80
|
const homeDir = options.homeDir || os.homedir()
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
await assertDirExists(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
81
|
+
const srcSkillsDir = join(packageRoot, 'skills')
|
|
82
|
+
const targets = [
|
|
83
|
+
{ name: 'codex', dir: join(homeDir, '.codex', 'skills') },
|
|
84
|
+
{ name: 'claude', dir: join(homeDir, '.claude', 'skills') },
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
await assertDirExists(srcSkillsDir, '模板目录 skills')
|
|
88
|
+
|
|
89
|
+
const stats = []
|
|
90
|
+
for (const target of targets) {
|
|
91
|
+
await ensureDir(target.dir)
|
|
92
|
+
await removeDeprecatedSkillDirs(target.dir)
|
|
93
|
+
const copyStats = await copyDirMerge({ srcDir: srcSkillsDir, dstDir: target.dir })
|
|
94
|
+
stats.push({ ...target, ...copyStats })
|
|
95
|
+
}
|
|
281
96
|
|
|
282
|
-
logger.success(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
`config.toml: 修复 ${configStats.changedKeys} 个配置项,新增 ${configStats.createdSections} 个分组 -> ${configStats.configPath}`,
|
|
287
|
-
)
|
|
97
|
+
logger.success('已初始化 skills 模板')
|
|
98
|
+
for (const target of stats) {
|
|
99
|
+
logger.info(`${target.name} skills: 覆盖复制 ${target.fileCount} 个文件 -> ${target.dir}`)
|
|
100
|
+
}
|
|
288
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ranger1/dx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.93",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"bin/",
|
|
20
20
|
"lib/",
|
|
21
|
-
"
|
|
21
|
+
"skills/",
|
|
22
22
|
"LICENSE",
|
|
23
23
|
"README.md",
|
|
24
24
|
"package.json"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-layering-audit-fixer
|
|
3
|
+
description: Use when backend、NestJS 分层架构或事务规范审查中,需要检查 Service 是否绕过 Repository 直接访问数据库、Controller 是否跨层注入 Repository、Service 是否伪装 Repository、事务装饰器是否匹配 afterCommit 使用、非 HTTP 场景事务模式是否正确、SSE 端点是否误加事务装饰器、以及是否存在直接 prisma.$transaction() 调用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 后端三层架构与事务规范合规检查
|
|
7
|
+
|
|
8
|
+
## 概览
|
|
9
|
+
|
|
10
|
+
扫描后端代码是否遵守 **Controller → Service → Repository** 三层架构约束及事务规范。默认只输出审计结果和修复建议;用户明确要求时才自动修复。
|
|
11
|
+
|
|
12
|
+
规则来源:`ruler/conventions.md` 第 4 节 "NestJS/Prisma 约定" + 第 5 节 "事务规范"。
|
|
13
|
+
|
|
14
|
+
## 检查项
|
|
15
|
+
|
|
16
|
+
### 分层架构
|
|
17
|
+
|
|
18
|
+
| 编号 | 违规类型 | 说明 |
|
|
19
|
+
|------|----------|------|
|
|
20
|
+
| L1 | Service 直接访问数据库 | Service 注入 `PrismaService` 并调用 `this.prisma.*` |
|
|
21
|
+
| L2 | Service 伪装为 Repository | Service 文件含 `getClient()` / `txHost.tx` 模式,实质是 Repository |
|
|
22
|
+
| L3 | Service 死依赖 | Service 注入 `PrismaService` 但从未使用 |
|
|
23
|
+
| L4 | Controller 跨层调用 | Controller 直接注入 Repository |
|
|
24
|
+
|
|
25
|
+
### 事务规范
|
|
26
|
+
|
|
27
|
+
| 编号 | 违规类型 | 说明 |
|
|
28
|
+
|------|----------|------|
|
|
29
|
+
| T1 | afterCommit 未被排空 | Controller 用 `@Transactional()` 但调用链中存在 `txEvents.afterCommit()` 回调(应改用 `@TransactionalWithAfterCommit()`) |
|
|
30
|
+
| T2 | Service 违规传播类型 | Service 使用 `Propagation.Required` / `RequiresNew` / `Nested`(禁止 Service 自行创建事务) |
|
|
31
|
+
| T3 | 直接 `prisma.$transaction()` | 绕过 `TransactionHost` 抽象,直接调用 `prisma.$transaction()` |
|
|
32
|
+
| T4 | SSE/流式端点加事务装饰器 | `@Sse()` 或流式返回的方法上使用了 `@Transactional()` / `@TransactionalWithAfterCommit()`(会导致 afterCommit 提前排空) |
|
|
33
|
+
| T5 | 非 HTTP 场景事务模式错误 | Subscriber/Scheduler 使用了 `@Transactional()` 装饰器而非 `txHost.withTransaction()` 或 `txEvents.withAfterCommit()` |
|
|
34
|
+
| T6 | 非 HTTP 场景缺少 CLS 作用域 | Scheduler 调用 `txHost.withTransaction()` 前未用 `cls.run()` 创建 CLS 作用域 |
|
|
35
|
+
|
|
36
|
+
## 审计命令
|
|
37
|
+
|
|
38
|
+
**分层架构扫描(并行执行):**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# L1 + L2 + L3:Service 注入 PrismaService
|
|
42
|
+
rg "PrismaService" apps/backend/src/modules --glob '*.service.ts' -l
|
|
43
|
+
|
|
44
|
+
# L2:Service 含 Repository 模式
|
|
45
|
+
rg "getClient|txHost\.tx" apps/backend/src/modules --glob '*.service.ts' -l
|
|
46
|
+
|
|
47
|
+
# L4:Controller 注入 Repository
|
|
48
|
+
rg "Repository" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**事务规范扫描(并行执行):**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# T1:找出所有只用 @Transactional() 的 Controller 方法(排除 @TransactionalWithAfterCommit)
|
|
55
|
+
rg "@Transactional\(\)" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
56
|
+
|
|
57
|
+
# T2:Service 违规传播类型
|
|
58
|
+
rg "Propagation\.(Required|RequiresNew|Nested)" apps/backend/src/modules --glob '*.service.ts' -l
|
|
59
|
+
|
|
60
|
+
# T3:直接 prisma.$transaction()
|
|
61
|
+
rg "prisma\.\$transaction" apps/backend/src/modules -l
|
|
62
|
+
|
|
63
|
+
# T4:SSE 端点是否带事务装饰器(需人工确认上下文)
|
|
64
|
+
rg "@Sse\(\)" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
65
|
+
|
|
66
|
+
# T5:Subscriber/Scheduler 误用 @Transactional 装饰器
|
|
67
|
+
rg "@Transactional" apps/backend/src/modules --glob '*.subscriber.ts' --glob '*.task.ts' --glob '*.scheduler*.ts' -l
|
|
68
|
+
|
|
69
|
+
# T6:Scheduler 使用 txHost.withTransaction 但未包 cls.run
|
|
70
|
+
rg "txHost\.withTransaction" apps/backend/src/modules --glob '*.scheduler*.ts' --glob '*.task.ts' -l
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 执行流程
|
|
74
|
+
|
|
75
|
+
1. 执行上述审计命令,收集命中文件列表
|
|
76
|
+
2. **分层检查**:对每个命中文件,读取构造函数和使用处,判定违规类型:
|
|
77
|
+
- 注入了 `PrismaService` 且有 `this.prisma.*` 调用 → **L1**
|
|
78
|
+
- 注入了 `PrismaService`/`txHost` 且有 `getClient()` → **L2**
|
|
79
|
+
- 注入了 `PrismaService` 但无任何 `this.prisma.*` 调用 → **L3**
|
|
80
|
+
- Controller 文件 import/注入 Repository → **L4**
|
|
81
|
+
3. **事务检查**:
|
|
82
|
+
- T1:对只用 `@Transactional()` 的 Controller,追踪其调用的 Service 方法,检查是否存在 `txEvents.afterCommit()` 调用
|
|
83
|
+
- T2:直接匹配即违规
|
|
84
|
+
- T3:直接匹配即违规
|
|
85
|
+
- T4:对 `@Sse()` 方法,检查同一方法上是否有事务装饰器
|
|
86
|
+
- T5:直接匹配即违规(Subscriber/Scheduler 不应用装饰器声明事务)
|
|
87
|
+
- T6:对命中文件,检查 `txHost.withTransaction()` 调用是否在 `cls.run()` 回调内部
|
|
88
|
+
4. 输出审计报告(按模块分组,标注违规类型和行号)
|
|
89
|
+
5. 仅当用户明确要求修复时,才执行修复
|
|
90
|
+
|
|
91
|
+
## 修复策略
|
|
92
|
+
|
|
93
|
+
### 分层架构
|
|
94
|
+
|
|
95
|
+
**L1:Service 直接访问数据库**
|
|
96
|
+
- 将数据库查询逻辑提取到对应 Repository(新建或复用已有)
|
|
97
|
+
- Service 改为注入 Repository 调用
|
|
98
|
+
|
|
99
|
+
**L2:Service 伪装为 Repository**
|
|
100
|
+
- 重命名文件为 `*.repository.ts`,类名改为 `*Repository`
|
|
101
|
+
- 更新所有引用(import、Module providers、注入处)
|
|
102
|
+
- 确认 Service 层消费者改为注入新 Repository
|
|
103
|
+
|
|
104
|
+
**L3:Service 死依赖**
|
|
105
|
+
- 移除构造函数中 `PrismaService` 注入
|
|
106
|
+
- 移除对应 import 语句
|
|
107
|
+
- 运行 lint 确认无残留
|
|
108
|
+
|
|
109
|
+
**L4:Controller 跨层调用**
|
|
110
|
+
- Controller 中的 Repository 调用下沉到 Service
|
|
111
|
+
- Controller 改为调用 Service 方法
|
|
112
|
+
|
|
113
|
+
### 事务规范
|
|
114
|
+
|
|
115
|
+
**T1:afterCommit 未被排空**
|
|
116
|
+
- 将 Controller 方法的 `@Transactional()` 改为 `@TransactionalWithAfterCommit()`
|
|
117
|
+
- 或确认调用链中确实不存在 `afterCommit()` 回调(则无需修改)
|
|
118
|
+
|
|
119
|
+
**T2:Service 违规传播类型**
|
|
120
|
+
- 改为 `Propagation.Mandatory`(必须在事务中调用)或 `Propagation.Supports`(有事务则加入)
|
|
121
|
+
- 事务边界上移到 Controller 层
|
|
122
|
+
|
|
123
|
+
**T3:直接 `prisma.$transaction()`**
|
|
124
|
+
- 改用 `txHost.withTransaction()` 或由 Controller 层 `@Transactional()` 声明事务边界
|
|
125
|
+
- Repository 内通过 `txHost.tx` 自动参与事务
|
|
126
|
+
|
|
127
|
+
**T4:SSE/流式端点加事务装饰器**
|
|
128
|
+
- 移除事务装饰器
|
|
129
|
+
- 如需事务操作,在流式逻辑内部用 `cls.run()` + `txHost.withTransaction()` 局部处理
|
|
130
|
+
|
|
131
|
+
**T5:非 HTTP 场景误用 @Transactional 装饰器**
|
|
132
|
+
- Subscriber:改用 `txEvents.withAfterCommit()` 包裹处理逻辑
|
|
133
|
+
- Scheduler/Task:改用 `cls.run()` + `txHost.withTransaction()`
|
|
134
|
+
|
|
135
|
+
**T6:非 HTTP 场景缺少 CLS 作用域**
|
|
136
|
+
- 在 `txHost.withTransaction()` 外层包裹 `cls.run()`:
|
|
137
|
+
```typescript
|
|
138
|
+
await this.cls.run(async () => {
|
|
139
|
+
await this.txHost.withTransaction(async () => { /* ... */ })
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 排除项
|
|
144
|
+
|
|
145
|
+
以下场景不视为违规:
|
|
146
|
+
- `*.repository.ts` 文件中使用 `PrismaService` / `getClient()` / `txHost`(Repository 正常职责)
|
|
147
|
+
- `prisma/` 目录下的基础设施文件(`prisma.service.ts` 等)
|
|
148
|
+
- `common/` 目录下的事务基础设施(`TransactionEventsService`、`AfterCommitInterceptor` 等)
|
|
149
|
+
- `*.spec.ts` / `e2e/` 测试文件
|
|
150
|
+
- Subscriber / Scheduler 中通过 `txHost.withTransaction()` 管理事务(正确模式,但其中数据库查询仍应走 Repository)
|
|
151
|
+
|
|
152
|
+
## 审计报告模板
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
## 后端分层架构与事务规范审计报告
|
|
156
|
+
|
|
157
|
+
### 违规汇总
|
|
158
|
+
|
|
159
|
+
**分层架构:**
|
|
160
|
+
- L1(Service 直接访问 DB):N 处
|
|
161
|
+
- L2(Service 伪装 Repository):N 处
|
|
162
|
+
- L3(Service 死依赖):N 处
|
|
163
|
+
- L4(Controller 跨层调用):N 处
|
|
164
|
+
|
|
165
|
+
**事务规范:**
|
|
166
|
+
- T1(afterCommit 未被排空):N 处
|
|
167
|
+
- T2(Service 违规传播类型):N 处
|
|
168
|
+
- T3(直接 prisma.$transaction):N 处
|
|
169
|
+
- T4(SSE 端点加事务装饰器):N 处
|
|
170
|
+
- T5(非 HTTP 场景误用装饰器):N 处
|
|
171
|
+
- T6(非 HTTP 缺少 CLS 作用域):N 处
|
|
172
|
+
|
|
173
|
+
### 详细列表
|
|
174
|
+
|
|
175
|
+
#### [模块名]
|
|
176
|
+
| 文件 | 违规类型 | 行号 | 说明 |
|
|
177
|
+
|------|----------|------|------|
|
|
178
|
+
| ... | L1 | 39 | `this.prisma.user.findMany()` |
|
|
179
|
+
| ... | T1 | 85 | `@Transactional()` 但调用链含 `afterCommit()` |
|
|
180
|
+
```
|
|
@@ -52,18 +52,11 @@ bash "$CODEX_HOME/skills/doctor/scripts/doctor.sh" --max-rounds 3
|
|
|
52
52
|
|
|
53
53
|
## 脚本职责
|
|
54
54
|
|
|
55
|
-
- 并行检测:`python3`、`python` 别名、`pnpm`、`dx`、`agent-browser`、`rg`、`multi_agent
|
|
55
|
+
- 并行检测:`python3`、`python` 别名、`pnpm`、`dx`、`agent-browser`、`rg`、`multi_agent`。
|
|
56
56
|
- 自动修复:按平台选择安装器修复缺失项。
|
|
57
|
-
- 自动修复:确保 `~/.codex/config.toml` 含以下目标值(缺失补齐、值不符覆盖):
|
|
58
|
-
- `[features] multi_agent = true`
|
|
59
|
-
- `[agents] max_threads = 15`
|
|
60
|
-
- `[agents.fixer] description/config_file`
|
|
61
|
-
- `[agents.orchestrator] description/config_file`
|
|
62
|
-
- `[agents.reviewer] description/config_file`
|
|
63
|
-
- `[agents.spark] description/config_file`
|
|
64
57
|
- 强制执行:每轮都运行 `pnpm add -g @ranger1/dx@latest && dx initial`。
|
|
65
58
|
- agent-browser:安装/升级并执行 Chromium 安装。
|
|
66
|
-
-
|
|
59
|
+
- 结果输出:展示每项状态、版本、关键信息;全部通过则退出 0,否则最多三轮后退出 1。
|
|
67
60
|
|
|
68
61
|
## 注意
|
|
69
62
|
|