@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.
Files changed (36) hide show
  1. package/lib/cli/commands/core.js +46 -1
  2. package/lib/cli/dx-cli.js +1 -0
  3. package/lib/codex-initial.js +79 -5
  4. package/lib/env.js +7 -3
  5. package/lib/exec.js +4 -1
  6. package/package.json +1 -1
  7. package/skills/backend-audit-fixer/SKILL.md +98 -0
  8. package/skills/backend-audit-fixer/references/backend-layering.md +103 -0
  9. package/skills/backend-audit-fixer/references/e2e.md +60 -0
  10. package/skills/backend-audit-fixer/references/env-accessor.md +73 -0
  11. package/skills/backend-audit-fixer/references/error-handling.md +77 -0
  12. package/skills/{naming-audit-fixer/references/fix-guide.md → backend-audit-fixer/references/naming-fix-guide.md} +8 -3
  13. package/skills/backend-audit-fixer/references/naming.md +139 -0
  14. package/skills/backend-audit-fixer/references/pagination-dto.md +52 -0
  15. package/skills/create-issue/SKILL.md +90 -0
  16. package/skills/issues-batch-deliver/SKILL.md +193 -84
  17. package/skills/pr-train-ship/SKILL.md +202 -49
  18. package/skills/backend-layering-audit-fixer/SKILL.md +0 -180
  19. package/skills/e2e-audit-fixer/SKILL.md +0 -76
  20. package/skills/e2e-audit-fixer/agents/openai.yaml +0 -4
  21. package/skills/env-accessor-audit-fixer/SKILL.md +0 -149
  22. package/skills/env-accessor-audit-fixer/agents/openai.yaml +0 -7
  23. package/skills/error-handling-audit-fixer/SKILL.md +0 -187
  24. package/skills/error-handling-audit-fixer/agents/openai.yaml +0 -7
  25. package/skills/naming-audit-fixer/SKILL.md +0 -149
  26. package/skills/pagination-dto-audit-fixer/SKILL.md +0 -69
  27. package/skills/pagination-dto-audit-fixer/agents/openai.yaml +0 -7
  28. /package/skills/{env-accessor-audit-fixer → backend-audit-fixer}/references/bootstrap-env-foundation.md +0 -0
  29. /package/skills/{error-handling-audit-fixer/references/foundation-bootstrap.md → backend-audit-fixer/references/error-handling-foundation-bootstrap.md} +0 -0
  30. /package/skills/{error-handling-audit-fixer → backend-audit-fixer}/references/error-handling-standard.md +0 -0
  31. /package/skills/{pagination-dto-audit-fixer → backend-audit-fixer}/references/pagination-standard.md +0 -0
  32. /package/skills/{e2e-audit-fixer/scripts/e2e_e2e_audit.py → backend-audit-fixer/scripts/e2e_audit.py} +0 -0
  33. /package/skills/{env-accessor-audit-fixer → backend-audit-fixer}/scripts/env_accessor_audit.py +0 -0
  34. /package/skills/{error-handling-audit-fixer → backend-audit-fixer}/scripts/error_handling_audit.py +0 -0
  35. /package/skills/{naming-audit-fixer/scripts/audit_naming.py → backend-audit-fixer/scripts/naming_audit.py} +0 -0
  36. /package/skills/{pagination-dto-audit-fixer → backend-audit-fixer}/scripts/pagination_dto_audit.py +0 -0
@@ -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 = fileCommand.replace('{TEST_PATH}', shellEscape(normalizedTestPath))
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
@@ -910,6 +910,7 @@ class DxCli {
910
910
  const options = {
911
911
  app: config.app,
912
912
  flags: effectiveFlags,
913
+ cwd: config.cwd,
913
914
  ports: config.ports || [],
914
915
  // 允许上游在 config.env 中注入环境变量(例如 NX_CACHE=false)
915
916
  env: config.env || {},
@@ -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 fs.lstat(target)
166
+ stat = await lstatIfExists(target)
121
167
  } catch (error) {
122
- if (error?.code === 'ENOENT') continue
123
- throw error
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 => `-e ${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 envFlags = envManager.buildEnvFlags(app, environment)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.105",
3
+ "version": "0.1.107",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
+ ```