@ranger1/dx 0.1.105 → 0.1.107
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/cli/commands/core.js +46 -1
- package/lib/cli/dx-cli.js +1 -0
- package/lib/codex-initial.js +79 -5
- package/lib/env.js +7 -3
- package/lib/exec.js +4 -1
- 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/issues-batch-deliver/SKILL.md +193 -84
- package/skills/pr-train-ship/SKILL.md +202 -49
- 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/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/cli/commands/core.js
CHANGED
|
@@ -76,6 +76,7 @@ export async function handleTest(cli, args) {
|
|
|
76
76
|
// 解析 -t 参数用于指定特定测试用例(使用原始参数列表)
|
|
77
77
|
const allArgs = cli.args // 使用原始参数列表包含所有标志
|
|
78
78
|
const testNamePattern = resolveTestNamePattern(allArgs)
|
|
79
|
+
const passthroughArgs = resolvePassthroughArgs(allArgs)
|
|
79
80
|
|
|
80
81
|
// 根据测试类型自动设置环境标志
|
|
81
82
|
if (type === 'e2e' && !cli.flags.e2e) {
|
|
@@ -108,16 +109,24 @@ export async function handleTest(cli, args) {
|
|
|
108
109
|
process.exit(1)
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
const directTarget = resolveNxTargetDirectCommand(cli, fileCommand, ['test:e2e'])
|
|
111
113
|
const normalizedTestPath = normalizeE2eTestPathForCommand(cli, fileCommand, testPath)
|
|
112
|
-
let command =
|
|
114
|
+
let command = directTarget
|
|
115
|
+
? `${directTarget.command} ${shellEscape(normalizedTestPath)}`
|
|
116
|
+
: fileCommand.replace('{TEST_PATH}', shellEscape(normalizedTestPath))
|
|
113
117
|
|
|
114
118
|
if (testNamePattern) {
|
|
115
119
|
command += ` -t ${shellEscape(testNamePattern)}`
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
if (passthroughArgs.length > 0) {
|
|
123
|
+
command += ` ${passthroughArgs.map(shellEscape).join(' ')}`
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
testConfig = {
|
|
119
127
|
...testConfig,
|
|
120
128
|
command: command,
|
|
129
|
+
...(directTarget?.cwd ? { cwd: directTarget.cwd } : {}),
|
|
121
130
|
description: testNamePattern
|
|
122
131
|
? `运行单个E2E测试文件的特定用例: ${testPath} -> ${testNamePattern}`
|
|
123
132
|
: `运行单个E2E测试文件: ${testPath}`
|
|
@@ -142,6 +151,10 @@ export async function handleTest(cli, args) {
|
|
|
142
151
|
forwardedArgs.push(`-t ${shellEscape(testNamePattern)}`)
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
if (passthroughArgs.length > 0) {
|
|
155
|
+
forwardedArgs.push(...passthroughArgs.map(shellEscape))
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
command += ` ${forwardedArgs.join(' ')}`
|
|
146
159
|
|
|
147
160
|
testConfig = {
|
|
@@ -177,6 +190,12 @@ function resolveTestNamePattern(args = []) {
|
|
|
177
190
|
return null
|
|
178
191
|
}
|
|
179
192
|
|
|
193
|
+
function resolvePassthroughArgs(args = []) {
|
|
194
|
+
const index = args.indexOf('--')
|
|
195
|
+
if (index === -1) return []
|
|
196
|
+
return args.slice(index + 1)
|
|
197
|
+
}
|
|
198
|
+
|
|
180
199
|
function shouldUseDirectPathArg(command) {
|
|
181
200
|
const text = String(command || '')
|
|
182
201
|
return (
|
|
@@ -228,6 +247,32 @@ function normalizeE2eTestPathForCommand(cli, command, testPath) {
|
|
|
228
247
|
return relativePath
|
|
229
248
|
}
|
|
230
249
|
|
|
250
|
+
function resolveNxTargetDirectCommand(cli, command, targetNames = []) {
|
|
251
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
252
|
+
const nxResolution = extractNxTarget(command, targetNames)
|
|
253
|
+
if (!nxResolution) return null
|
|
254
|
+
|
|
255
|
+
const projectDir = join(projectRoot, 'apps', nxResolution.project)
|
|
256
|
+
const projectConfigPath = join(projectDir, 'project.json')
|
|
257
|
+
if (!existsSync(projectConfigPath)) return null
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf8'))
|
|
261
|
+
const resolvedTarget = projectConfig?.targets?.[nxResolution.target]
|
|
262
|
+
const directCommand = resolvedTarget?.options?.command
|
|
263
|
+
if (typeof directCommand !== 'string' || directCommand.trim().length === 0) {
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
const cwd = resolvedTarget?.options?.cwd
|
|
267
|
+
return {
|
|
268
|
+
command: directCommand.trim(),
|
|
269
|
+
cwd: typeof cwd === 'string' && cwd.trim().length > 0 ? cwd.trim() : null,
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
231
276
|
function resolveNxTargetProjectCwd(cli, command, targetNames = []) {
|
|
232
277
|
const projectRoot = cli?.projectRoot || process.cwd()
|
|
233
278
|
const nxResolution = extractNxTarget(command, targetNames)
|
package/lib/cli/dx-cli.js
CHANGED
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/lib/env.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
2
|
+
import { isAbsolute, join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
function resolveProjectRoot() {
|
|
5
5
|
return process.env.DX_PROJECT_ROOT || process.cwd()
|
|
@@ -101,9 +101,13 @@ export class EnvManager {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// 构建dotenv命令参数
|
|
104
|
-
buildEnvFlags(app, environment) {
|
|
104
|
+
buildEnvFlags(app, environment, options = {}) {
|
|
105
|
+
const absolute = Boolean(options.absolute)
|
|
105
106
|
return this.getResolvedEnvLayers(app, environment)
|
|
106
|
-
.map(layer =>
|
|
107
|
+
.map(layer => {
|
|
108
|
+
const envFile = absolute && !isAbsolute(layer) ? join(this.projectRoot, layer) : layer
|
|
109
|
+
return `-e ${envFile}`
|
|
110
|
+
})
|
|
107
111
|
.join(' ')
|
|
108
112
|
}
|
|
109
113
|
|
package/lib/exec.js
CHANGED
|
@@ -163,7 +163,10 @@ export class ExecManager {
|
|
|
163
163
|
logger.info(`dotenv层 ${envLabel}: ${layerSummary}`, '🌱')
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
const
|
|
166
|
+
const commandCwd = cwd || process.cwd()
|
|
167
|
+
const envFlags = envManager.buildEnvFlags(app, environment, {
|
|
168
|
+
absolute: commandCwd !== process.cwd(),
|
|
169
|
+
})
|
|
167
170
|
if (envFlags) {
|
|
168
171
|
// 对 build 命令禁用 dotenv 的 --override,避免覆盖我们显式传入的 NODE_ENV
|
|
169
172
|
const overrideFlag = isBuildCmdForWrapping ? '' : '--override'
|
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
|
+
```
|