@ranger1/dx 0.1.91 → 0.1.92

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 (52) hide show
  1. package/README.md +2 -2
  2. package/lib/cli/help.js +1 -1
  3. package/lib/codex-initial.js +19 -215
  4. package/package.json +2 -2
  5. package/skills/backend-layering-audit-fixer/SKILL.md +180 -0
  6. package/{codex/skills → skills}/doctor/SKILL.md +2 -9
  7. package/{codex/skills → skills}/doctor/scripts/doctor.sh +2 -253
  8. package/skills/git-pr-ship/SKILL.md +481 -0
  9. package/skills/naming-audit-fixer/SKILL.md +149 -0
  10. package/skills/naming-audit-fixer/references/fix-guide.md +93 -0
  11. package/skills/naming-audit-fixer/scripts/audit_naming.py +534 -0
  12. package/codex/agents/fixer.toml +0 -37
  13. package/codex/agents/orchestrator.toml +0 -11
  14. package/codex/agents/reviewer.toml +0 -52
  15. package/codex/agents/spark.toml +0 -18
  16. package/codex/skills/pr-review-loop/SKILL.md +0 -209
  17. package/codex/skills/pr-review-loop/agents/openai.yaml +0 -4
  18. package/codex/skills/pr-review-loop/references/agents/pr-context.md +0 -73
  19. package/codex/skills/pr-review-loop/references/agents/pr-precheck.md +0 -161
  20. package/codex/skills/pr-review-loop/references/agents/pr-review-aggregate.md +0 -188
  21. package/codex/skills/pr-review-loop/references/skill-layout.md +0 -25
  22. package/codex/skills/pr-review-loop/scripts/gh_review_harvest.py +0 -292
  23. package/codex/skills/pr-review-loop/scripts/pr_context.py +0 -351
  24. package/codex/skills/pr-review-loop/scripts/pr_review_aggregate.py +0 -951
  25. package/codex/skills/pr-review-loop/scripts/test_pr_review_aggregate.py +0 -876
  26. package/codex/skills/pr-review-loop/scripts/test_validate_reviewer_prompts.py +0 -92
  27. package/codex/skills/pr-review-loop/scripts/validate_reviewer_prompts.py +0 -87
  28. /package/{codex/skills → skills}/doctor/agents/openai.yaml +0 -0
  29. /package/{codex/skills → skills}/e2e-audit-fixer/SKILL.md +0 -0
  30. /package/{codex/skills → skills}/e2e-audit-fixer/agents/openai.yaml +0 -0
  31. /package/{codex/skills → skills}/e2e-audit-fixer/scripts/e2e_e2e_audit.py +0 -0
  32. /package/{codex/skills → skills}/env-accessor-audit-fixer/SKILL.md +0 -0
  33. /package/{codex/skills → skills}/env-accessor-audit-fixer/agents/openai.yaml +0 -0
  34. /package/{codex/skills → skills}/env-accessor-audit-fixer/references/bootstrap-env-foundation.md +0 -0
  35. /package/{codex/skills → skills}/env-accessor-audit-fixer/scripts/env_accessor_audit.py +0 -0
  36. /package/{codex/skills → skills}/error-handling-audit-fixer/SKILL.md +0 -0
  37. /package/{codex/skills → skills}/error-handling-audit-fixer/agents/openai.yaml +0 -0
  38. /package/{codex/skills → skills}/error-handling-audit-fixer/references/error-handling-standard.md +0 -0
  39. /package/{codex/skills → skills}/error-handling-audit-fixer/references/foundation-bootstrap.md +0 -0
  40. /package/{codex/skills → skills}/error-handling-audit-fixer/scripts/error_handling_audit.py +0 -0
  41. /package/{codex/skills → skills}/gh-dependabot-cleanup/SKILL.md +0 -0
  42. /package/{codex/skills → skills}/gh-dependabot-cleanup/agents/openai.yaml +0 -0
  43. /package/{codex/skills → skills}/git-commit-and-pr/SKILL.md +0 -0
  44. /package/{codex/skills → skills}/git-commit-and-pr/agents/openai.yaml +0 -0
  45. /package/{codex/skills → skills}/git-release/SKILL.md +0 -0
  46. /package/{codex/skills → skills}/git-release/agents/openai.yaml +0 -0
  47. /package/{codex/skills → skills}/online-debug-guard/SKILL.md +0 -0
  48. /package/{codex/skills → skills}/online-debug-guard/agents/openai.yaml +0 -0
  49. /package/{codex/skills → skills}/pagination-dto-audit-fixer/SKILL.md +0 -0
  50. /package/{codex/skills → skills}/pagination-dto-audit-fixer/agents/openai.yaml +0 -0
  51. /package/{codex/skills → skills}/pagination-dto-audit-fixer/references/pagination-standard.md +0 -0
  52. /package/{codex/skills → skills}/pagination-dto-audit-fixer/scripts/pagination_dto_audit.py +0 -0
@@ -0,0 +1,93 @@
1
+ # 修复指南:文件/文件夹重命名与引用更新
2
+
3
+ ## 核心原则
4
+
5
+ 1. **目录优先于文件**:先重命名目录,再处理其下的文件
6
+ 2. **git mv 保留历史**:所有重命名必须用 `git mv`,不要用 `mv` + `git add`
7
+ 3. **引用同步更新**:每次重命名后立即更新所有 import/require 引用
8
+ 4. **增量验证**:每完成一个模块的重命名,立即运行 lint + build 验证
9
+
10
+ ## 修复流程
11
+
12
+ ### Step 1: 目录重命名
13
+
14
+ 目录重命名影响最大,优先处理。
15
+
16
+ ```bash
17
+ # 示例:重命名 NestJS 模块目录
18
+ git mv apps/backend/src/modules/ai.model apps/backend/src/modules/ai-model
19
+ ```
20
+
21
+ 重命名后,使用 Grep 搜索所有引用该目录路径的导入语句:
22
+
23
+ ```
24
+ # 搜索模式
25
+ from '.*/ai\.model/ → 更新为 /ai-model/
26
+ from '.*/ai\.usecase/ → 更新为 /ai-usecase/
27
+ ```
28
+
29
+ ### Step 2: 文件重命名(按模块分批)
30
+
31
+ 每次处理一个模块目录下的所有违规文件:
32
+
33
+ ```bash
34
+ # 示例:重命名 DTO 文件
35
+ git mv "apps/backend/src/modules/chat/dto/requests/send.message.request.dto.ts" \
36
+ "apps/backend/src/modules/chat/dto/requests/send-message-request.dto.ts"
37
+ ```
38
+
39
+ ### Step 3: 更新导入引用
40
+
41
+ 对每个被重命名的文件,搜索并更新所有导入:
42
+
43
+ 1. 用 Grep 搜索旧文件名(不含扩展名)的 import 语句
44
+ 2. 用 Edit 逐一更新为新文件名
45
+ 3. 注意 barrel export(index.ts)中的 re-export 也需要更新
46
+
47
+ ### Step 4: 验证
48
+
49
+ ```bash
50
+ # NestJS 后端
51
+ dx lint
52
+ dx build backend --dev
53
+
54
+ # 前端
55
+ dx build front --dev
56
+ dx build admin --dev
57
+ ```
58
+
59
+ ## 各框架规范速查
60
+
61
+ ### NestJS
62
+
63
+ | 元素 | 规范 | 示例 |
64
+ |------|------|------|
65
+ | 文件 | `kebab-name.type.ts` | `user-activity.service.ts` |
66
+ | 目录 | kebab-case | `user-statistics/` |
67
+ | DTO | `kebab-name.request.dto.ts` | `send-message.request.dto.ts` |
68
+ | Exception | `kebab-name.exception.ts` | `message-not-found.exception.ts` |
69
+ | Spec | `kebab-name.type.spec.ts` | `user.service.spec.ts` |
70
+ | E2E | `kebab-name.e2e-spec.ts` | `activity-admin.e2e-spec.ts` |
71
+
72
+ ### Next.js + React
73
+
74
+ | 元素 | 规范 | 示例 |
75
+ |------|------|------|
76
+ | 组件 .tsx | PascalCase | `ChatPanel.tsx` |
77
+ | 工具 .ts | kebab-case | `sort-mappings.ts` |
78
+ | Hook .ts | camelCase | `useChat.ts` |
79
+ | 路由文件 | 小写 | `page.tsx`, `layout.tsx` |
80
+ | 目录 | kebab-case | `character/`(单数) |
81
+ | ui/ | kebab-case | `button.tsx` |
82
+
83
+ ### Vite + React
84
+
85
+ 与 Next.js 相同,但无路由文件约束。
86
+
87
+ ## 高风险操作注意
88
+
89
+ - **barrel export (index.ts)**:重命名后检查是否有 `export * from './old-name'`
90
+ - **动态 import**:`import()` 表达式中的路径也需要更新
91
+ - **tsconfig paths**:如果 tsconfig 中配置了路径别名指向具体文件,也需更新
92
+ - **测试文件**:jest.config 或 vitest.config 中的 testMatch / include 模式可能引用目录名
93
+ - **Prisma schema**:`schema/` 下的 `.prisma` 文件名通常与模块名对应,但 Prisma 不强制命名规范
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ naming-convention-audit: 按模型指定的规则扫描文件/文件夹命名违规。
4
+
5
+ 本脚本不做框架检测,所有决策由调用方(模型)通过 JSON 配置传入。
6
+ 脚本只负责:遍历目录 → 按规则匹配 → 输出 JSON 报告。
7
+
8
+ 用法:
9
+ echo '<config_json>' | python audit_naming.py
10
+ python audit_naming.py config.json
11
+
12
+ 配置 JSON 格式:
13
+ {
14
+ "scans": [
15
+ {
16
+ "root": "apps/backend/src",
17
+ "framework": "nestjs",
18
+ "extra_skip_dirs": ["generated"],
19
+ "extra_ok_files": ["bootstrap.ts"],
20
+ "ui_dirs": []
21
+ },
22
+ {
23
+ "root": "apps/front/src",
24
+ "framework": "nextjs-react",
25
+ "app_dir": "app",
26
+ "ui_dirs": ["components/ui"]
27
+ }
28
+ ]
29
+ }
30
+
31
+ 支持的 framework 值:
32
+ - nestjs : NestJS 后端(kebab-name.type.ts)
33
+ - nextjs-react : Next.js + React(PascalCase .tsx, kebab .ts, 路由文件豁免)
34
+ - react : React SPA / Vite(PascalCase .tsx, kebab .ts)
35
+ - vue : Vue(kebab-case .vue, kebab .ts)
36
+ - angular : Angular(kebab-name.type.ts,类似 NestJS)
37
+ - generic-ts : 通用 TypeScript(全 kebab-case,识别标准后缀)
38
+ """
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ import os
43
+ import re
44
+ import sys
45
+ from dataclasses import asdict, dataclass
46
+ from pathlib import Path
47
+
48
+ # ── 默认跳过 ──────────────────────────────────────────────
49
+
50
+ DEFAULT_SKIP_DIRS = frozenset({
51
+ 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.output',
52
+ '.turbo', '.nx', 'coverage', '.cache', '__pycache__', '.omc', '.claude',
53
+ '.vscode', '.idea', 'generated', 'migrations',
54
+ })
55
+
56
+ DEFAULT_SKIP_FILES = frozenset({'.DS_Store', 'thumbs.db', '.gitkeep'})
57
+
58
+ DEFAULT_OK_FILES = frozenset({
59
+ 'index.ts', 'index.tsx', 'index.js', 'index.jsx',
60
+ 'main.ts', 'main.tsx', 'main.js', 'main.jsx',
61
+ 'types.ts', 'types.d.ts', 'constants.ts',
62
+ 'seed.ts', 'schema.prisma', 'App.tsx', 'App.ts',
63
+ })
64
+
65
+ # 合法的特殊目录名(不检查 kebab-case)
66
+ OK_DIR_NAMES = frozenset({
67
+ 'dto', 'e2e', 'src', '@types', '__tests__', '__mocks__',
68
+ 'ui', 'app', 'public', 'pages', 'assets', 'static',
69
+ })
70
+
71
+ # ── NestJS 类型后缀(最长优先) ──────────────────────────────
72
+
73
+ NESTJS_COMPOUND_SUFFIXES = [
74
+ '.e2e-spec.ts', '.response.dto.ts', '.request.dto.ts',
75
+ '.exception.spec.ts', '.service.spec.ts', '.controller.spec.ts',
76
+ '.repository.spec.ts', '.guard.spec.ts',
77
+ ]
78
+ NESTJS_SIMPLE_SUFFIXES = [
79
+ '.controller.ts', '.repository.ts', '.interceptor.ts', '.middleware.ts',
80
+ '.subscriber.ts', '.decorator.ts', '.interface.ts', '.exception.ts',
81
+ '.constants.ts', '.constant.ts', '.strategy.ts', '.provider.ts',
82
+ '.adapter.ts', '.factory.ts', '.service.ts', '.module.ts',
83
+ '.helper.ts', '.entity.ts', '.guard.ts', '.pipe.ts',
84
+ '.filter.ts', '.enum.ts', '.util.ts', '.config.ts',
85
+ '.spec.ts', '.type.ts', '.types.ts', '.dto.ts',
86
+ ]
87
+ NESTJS_ALL_SUFFIXES = NESTJS_COMPOUND_SUFFIXES + NESTJS_SIMPLE_SUFFIXES
88
+
89
+ # Next.js 路由文件
90
+ NEXTJS_ROUTE_FILES = frozenset({
91
+ 'page.tsx', 'page.ts', 'page.jsx', 'page.js',
92
+ 'layout.tsx', 'layout.ts', 'layout.jsx', 'layout.js',
93
+ 'loading.tsx', 'loading.ts', 'error.tsx', 'error.ts',
94
+ 'not-found.tsx', 'not-found.ts', 'template.tsx', 'template.ts',
95
+ 'default.tsx', 'default.ts', 'route.tsx', 'route.ts',
96
+ 'middleware.ts', 'middleware.js', 'global-error.tsx',
97
+ 'sitemap.ts', 'robots.ts', 'opengraph-image.tsx', 'icon.tsx',
98
+ 'apple-icon.tsx', 'manifest.ts',
99
+ })
100
+
101
+ # Next.js app 目录下常见的非路由客户端页面文件名(kebab-case 合法)
102
+ NEXTJS_CLIENT_PAGE_FILES = frozenset({
103
+ 'client-page.tsx', 'client-page.ts',
104
+ })
105
+
106
+ # 前端 .ts 文件允许的 name.type 模式中的 type 段
107
+ FRONTEND_TYPE_SUFFIXES = frozenset({
108
+ 'helpers', 'helper', 'utils', 'util', 'config', 'storage',
109
+ 'context', 'types', 'schema', 'styles', 'constants',
110
+ 'mock', 'mocks', 'fixture', 'fixtures', 'service',
111
+ })
112
+
113
+ # 通用 TS 项目中合法的文件类型后缀(name.type.ts 模式中的 type 段)
114
+ # 这些后缀用点号分隔是标准模式,不应视为 kebab-case 违规
115
+ GENERIC_TS_TYPE_SUFFIXES = frozenset({
116
+ 'spec', 'test', 'e2e-spec',
117
+ 'service', 'module', 'controller', 'repository',
118
+ 'guard', 'pipe', 'filter', 'interceptor', 'middleware',
119
+ 'decorator', 'interface', 'exception', 'entity',
120
+ 'enum', 'type', 'types', 'dto', 'config', 'constant', 'constants',
121
+ 'util', 'utils', 'helper', 'helpers', 'factory', 'strategy',
122
+ 'subscriber', 'adapter', 'provider', 'mock', 'mocks',
123
+ 'fixture', 'fixtures', 'schema', 'model',
124
+ 'handler', 'resolver', 'directive', 'component',
125
+ })
126
+
127
+ # Angular 类型后缀
128
+ ANGULAR_SUFFIXES = [
129
+ '.component.spec.ts', '.service.spec.ts', '.pipe.spec.ts',
130
+ '.directive.spec.ts', '.guard.spec.ts', '.resolver.spec.ts',
131
+ '.component.ts', '.component.html', '.component.css', '.component.scss',
132
+ '.service.ts', '.module.ts', '.pipe.ts', '.directive.ts',
133
+ '.guard.ts', '.resolver.ts', '.interceptor.ts', '.model.ts',
134
+ '.interface.ts', '.enum.ts', '.spec.ts',
135
+ ]
136
+
137
+
138
+ # ── 数据结构 ──────────────────────────────────────────────
139
+
140
+ @dataclass
141
+ class Violation:
142
+ path: str
143
+ kind: str # file | directory
144
+ rule: str
145
+ message: str
146
+ current: str
147
+ suggested: str | None
148
+ severity: str # info | warning
149
+
150
+
151
+ # ── 判断工具 ──────────────────────────────────────────────
152
+
153
+ def is_kebab(name: str) -> bool:
154
+ return bool(re.match(r'^[a-z][a-z0-9]*(-[a-z0-9]+)*$', name))
155
+
156
+ def is_pascal(name: str) -> bool:
157
+ return bool(re.match(r'^[A-Z][a-zA-Z0-9]+$', name))
158
+
159
+ def is_camel(name: str) -> bool:
160
+ return bool(re.match(r'^[a-z][a-zA-Z0-9]+$', name))
161
+
162
+ def is_numeric(name: str) -> bool:
163
+ """纯数字目录名(如 403, 404, 500)合法。"""
164
+ return bool(re.match(r'^\d+$', name))
165
+
166
+ def to_kebab(name: str) -> str:
167
+ s = re.sub(r'([A-Z])', r'-\1', name).lower().lstrip('-')
168
+ s = s.replace('.', '-').replace('_', '-')
169
+ return re.sub(r'-+', '-', s)
170
+
171
+ def to_pascal(name: str) -> str:
172
+ parts = re.split(r'[-_.]', name)
173
+ return ''.join(p.capitalize() for p in parts if p)
174
+
175
+
176
+ def _is_in_ui_dir(rel_path: str, scan_root_rel: str, ui_dirs: list[str]) -> bool:
177
+ """检查文件是否位于 UI 组件目录(如 shadcn 的 components/ui/)。"""
178
+ for ui_dir in ui_dirs:
179
+ full_ui = scan_root_rel + '/' + ui_dir if scan_root_rel != '.' else ui_dir
180
+ if rel_path.startswith(full_ui + '/') or ('/' + ui_dir + '/') in rel_path:
181
+ return True
182
+ return False
183
+
184
+
185
+ # ── NestJS 检查 ───────────────────────────────────────────
186
+
187
+ def _extract_suffix(filename: str, suffixes: list[str]) -> tuple[str, str | None]:
188
+ for suffix in suffixes:
189
+ if filename.endswith(suffix):
190
+ return filename[:-len(suffix)], suffix
191
+ base = filename.rsplit('.', 1)[0] if '.' in filename else filename
192
+ return base, None
193
+
194
+
195
+ def check_nestjs_file(fn: str, rel: str, ok_files: frozenset) -> list[Violation]:
196
+ vs: list[Violation] = []
197
+ if not fn.endswith('.ts') or fn.endswith('.d.ts') or fn in ok_files:
198
+ return vs
199
+ name, suffix = _extract_suffix(fn, NESTJS_ALL_SUFFIXES)
200
+ if suffix:
201
+ if name and '.' in name:
202
+ vs.append(Violation(
203
+ path=rel, kind='file', rule='nestjs-name-dots',
204
+ message='name 部分应使用 kebab-case(连字符),不应使用点号',
205
+ current=fn, suggested=name.replace('.', '-') + suffix, severity='warning'))
206
+ elif name and not is_kebab(name) and name not in ('', 'i18n'):
207
+ vs.append(Violation(
208
+ path=rel, kind='file', rule='nestjs-name-case',
209
+ message='name 部分应使用 kebab-case',
210
+ current=fn, suggested=to_kebab(name) + suffix, severity='warning'))
211
+ else:
212
+ if name and '.' in name:
213
+ vs.append(Violation(
214
+ path=rel, kind='file', rule='nestjs-name-dots',
215
+ message='文件名包含多余的点号',
216
+ current=fn, suggested=name.replace('.', '-') + '.ts', severity='info'))
217
+ return vs
218
+
219
+
220
+ def check_nestjs_dir(dn: str, rel: str) -> list[Violation]:
221
+ vs: list[Violation] = []
222
+ # 跳过 @types、纯数字等合法特殊目录名
223
+ if dn in OK_DIR_NAMES or dn.startswith('@') or is_numeric(dn):
224
+ return vs
225
+ if '.' in dn:
226
+ vs.append(Violation(
227
+ path=rel, kind='directory', rule='nestjs-dir-dots',
228
+ message='目录名不应包含点号,应使用 kebab-case',
229
+ current=dn, suggested=dn.replace('.', '-'), severity='warning'))
230
+ elif not is_kebab(dn):
231
+ vs.append(Violation(
232
+ path=rel, kind='directory', rule='nestjs-dir-case',
233
+ message='目录名应使用 kebab-case',
234
+ current=dn, suggested=to_kebab(dn), severity='warning'))
235
+ return vs
236
+
237
+
238
+ # ── Angular 检查 ──────────────────────────────────────────
239
+
240
+ def check_angular_file(fn: str, rel: str, ok_files: frozenset) -> list[Violation]:
241
+ vs: list[Violation] = []
242
+ if fn in ok_files:
243
+ return vs
244
+ name, suffix = _extract_suffix(fn, ANGULAR_SUFFIXES)
245
+ if suffix:
246
+ if name and '.' in name:
247
+ vs.append(Violation(
248
+ path=rel, kind='file', rule='angular-name-dots',
249
+ message='name 部分应使用 kebab-case',
250
+ current=fn, suggested=name.replace('.', '-') + suffix, severity='warning'))
251
+ elif name and not is_kebab(name):
252
+ vs.append(Violation(
253
+ path=rel, kind='file', rule='angular-name-case',
254
+ message='name 部分应使用 kebab-case',
255
+ current=fn, suggested=to_kebab(name) + suffix, severity='warning'))
256
+ return vs
257
+
258
+
259
+ # ── React / Next.js 检查 ─────────────────────────────────
260
+
261
+ def check_tsx_file(fn: str, rel: str, ok_files: frozenset,
262
+ is_app_dir: bool, has_nextjs: bool,
263
+ in_ui_dir: bool) -> list[Violation]:
264
+ vs: list[Violation] = []
265
+ if not fn.endswith('.tsx') or fn in ok_files:
266
+ return vs
267
+ base = fn[:-4]
268
+ if base == 'index':
269
+ return vs
270
+ # Next.js app 目录下的路由文件和客户端页面文件豁免
271
+ if has_nextjs and is_app_dir:
272
+ if fn in NEXTJS_ROUTE_FILES or fn in NEXTJS_CLIENT_PAGE_FILES:
273
+ return vs
274
+ # 动态路由 [id].tsx, [...slug].tsx
275
+ if base.startswith('['):
276
+ return vs
277
+ # 测试文件跳过
278
+ if base.endswith('.test') or base.endswith('.spec'):
279
+ return vs
280
+ # ui/ 目录下的 shadcn 组件保持 kebab-case,不要求 PascalCase
281
+ if in_ui_dir:
282
+ if not is_kebab(base):
283
+ vs.append(Violation(
284
+ path=rel, kind='file', rule='react-ui-kebab',
285
+ message='ui/ 目录下的组件应保持 kebab-case',
286
+ current=fn, suggested=to_kebab(base) + '.tsx', severity='info'))
287
+ return vs
288
+ # 非 ui/ 下的 .tsx 组件应为 PascalCase
289
+ if not is_pascal(base):
290
+ vs.append(Violation(
291
+ path=rel, kind='file', rule='react-tsx-pascal',
292
+ message='.tsx 组件文件应使用 PascalCase',
293
+ current=fn, suggested=to_pascal(base) + '.tsx', severity='warning'))
294
+ return vs
295
+
296
+
297
+ def check_ts_file(fn: str, rel: str, ok_files: frozenset) -> list[Violation]:
298
+ vs: list[Violation] = []
299
+ if not fn.endswith('.ts') or fn.endswith('.d.ts') or fn in ok_files:
300
+ return vs
301
+ base = fn[:-3]
302
+
303
+ # hook 文件 → camelCase(含 hook 的 spec/test 文件也豁免)
304
+ if base.startswith('use') and len(base) > 3 and base[3:4].isupper():
305
+ # useXxx.ts / useXxx.spec.ts / useXxx.test.ts 都合法
306
+ clean_hook = re.sub(r'\.(spec|test)$', '', base)
307
+ if not is_camel(clean_hook):
308
+ vs.append(Violation(
309
+ path=rel, kind='file', rule='react-hook-camel',
310
+ message='Hook 文件应使用 camelCase(useXxx.ts)',
311
+ current=fn, suggested=None, severity='warning'))
312
+ return vs
313
+
314
+ # 去掉 .spec / .test 后缀
315
+ clean = re.sub(r'\.(spec|test)$', '', base)
316
+
317
+ # 允许 name.type.ts 模式(如 foo.helpers.ts)
318
+ parts = clean.rsplit('.', 1)
319
+ if len(parts) == 2 and parts[1] in FRONTEND_TYPE_SUFFIXES:
320
+ name_part = parts[0]
321
+ if name_part and not is_kebab(name_part):
322
+ tail = base[len(clean):]
323
+ vs.append(Violation(
324
+ path=rel, kind='file', rule='react-ts-kebab',
325
+ message='.ts 文件 name 部分应使用 kebab-case',
326
+ current=fn, suggested=to_kebab(name_part) + '.' + parts[1] + tail + '.ts',
327
+ severity='warning'))
328
+ elif clean and not is_kebab(clean):
329
+ tail = base[len(clean):]
330
+ vs.append(Violation(
331
+ path=rel, kind='file', rule='react-ts-kebab',
332
+ message='.ts 非组件文件应使用 kebab-case',
333
+ current=fn, suggested=to_kebab(clean) + tail + '.ts', severity='warning'))
334
+ return vs
335
+
336
+
337
+ def check_vue_file(fn: str, rel: str, ok_files: frozenset) -> list[Violation]:
338
+ vs: list[Violation] = []
339
+ if not fn.endswith('.vue') or fn in ok_files:
340
+ return vs
341
+ base = fn[:-4]
342
+ if not is_pascal(base) and not is_kebab(base):
343
+ vs.append(Violation(
344
+ path=rel, kind='file', rule='vue-component-case',
345
+ message='.vue 组件应使用 PascalCase 或 kebab-case',
346
+ current=fn, suggested=to_pascal(base) + '.vue', severity='warning'))
347
+ return vs
348
+
349
+
350
+ def check_frontend_dir(dn: str, rel: str) -> list[Violation]:
351
+ vs: list[Violation] = []
352
+ # 跳过特殊前缀目录
353
+ if dn[0:1] in ('[', '(', '@', '_'):
354
+ return vs
355
+ # 跳过已知合法目录名
356
+ if dn in OK_DIR_NAMES:
357
+ return vs
358
+ # 跳过纯数字目录(如 403, 404, 500 错误页)
359
+ if is_numeric(dn):
360
+ return vs
361
+ if not is_kebab(dn):
362
+ vs.append(Violation(
363
+ path=rel, kind='directory', rule='frontend-dir-kebab',
364
+ message='目录应使用 kebab-case',
365
+ current=dn, suggested=to_kebab(dn), severity='warning'))
366
+ # components/ 下检查单数
367
+ if dn.endswith('s') and not dn.endswith('ss') and len(dn) > 3:
368
+ parent_name = Path(rel).parent.name
369
+ if parent_name == 'components':
370
+ vs.append(Violation(
371
+ path=rel, kind='directory', rule='frontend-dir-singular',
372
+ message='components/ 下子目录建议使用单数',
373
+ current=dn, suggested=dn[:-1], severity='info'))
374
+ return vs
375
+
376
+
377
+ def check_generic_ts(fn: str, rel: str, ok_files: frozenset) -> list[Violation]:
378
+ """通用 TS 检查:识别 name.type.ext 模式,只对 name 部分检查 kebab-case。"""
379
+ vs: list[Violation] = []
380
+ if fn in ok_files or fn.endswith('.d.ts'):
381
+ return vs
382
+ if not (fn.endswith('.ts') or fn.endswith('.tsx') or fn.endswith('.js') or fn.endswith('.jsx')):
383
+ return vs
384
+
385
+ ext = '.' + fn.rsplit('.', 1)[1] # .ts / .tsx / .js / .jsx
386
+ base = fn[:-len(ext)]
387
+
388
+ if base == 'index':
389
+ return vs
390
+
391
+ # 逐层剥离已知 type 后缀,提取 name 部分
392
+ # 如 "command.handler.test" → name="command", suffixes=["handler", "test"]
393
+ # 如 "error-codes.spec" → name="error-codes", suffixes=["spec"]
394
+ remaining = base
395
+ while True:
396
+ parts = remaining.rsplit('.', 1)
397
+ if len(parts) == 2 and parts[1] in GENERIC_TS_TYPE_SUFFIXES:
398
+ remaining = parts[0]
399
+ else:
400
+ break
401
+
402
+ name_part = remaining
403
+
404
+ # 如果整个 base 就是一个 type 后缀(如 "types.ts"),跳过
405
+ if not name_part:
406
+ return vs
407
+
408
+ # 检查 name 部分是否 kebab-case
409
+ if not is_kebab(name_part):
410
+ suggested_name = to_kebab(name_part)
411
+ # 重建文件名:kebab-name + 原有后缀部分
412
+ suffix_part = base[len(name_part):] # 如 ".handler.test" 或 ".spec"
413
+ suggested = suggested_name + suffix_part + ext
414
+ vs.append(Violation(
415
+ path=rel, kind='file', rule='generic-ts-kebab',
416
+ message='文件名的 name 部分应使用 kebab-case',
417
+ current=fn, suggested=suggested, severity='warning'))
418
+
419
+ return vs
420
+
421
+
422
+ # ── 扫描引擎 ─────────────────────────────────────────────
423
+
424
+ def scan(config: dict) -> dict:
425
+ """按配置扫描,返回结果字典。"""
426
+ project_root = Path.cwd().resolve()
427
+ all_violations: list[Violation] = []
428
+
429
+ for scan_cfg in config.get('scans', []):
430
+ root_rel = scan_cfg['root']
431
+ framework = scan_cfg.get('framework', 'generic-ts')
432
+ extra_skip = frozenset(scan_cfg.get('extra_skip_dirs', []))
433
+ extra_ok = frozenset(scan_cfg.get('extra_ok_files', []))
434
+ app_dir_name = scan_cfg.get('app_dir', 'app')
435
+ ui_dirs: list[str] = scan_cfg.get('ui_dirs', [])
436
+
437
+ skip_dirs = DEFAULT_SKIP_DIRS | extra_skip
438
+ ok_files = DEFAULT_OK_FILES | extra_ok | DEFAULT_SKIP_FILES
439
+
440
+ scan_root = project_root / root_rel
441
+ if not scan_root.exists():
442
+ continue
443
+
444
+ app_dir_path = scan_root / app_dir_name
445
+ has_nextjs = framework == 'nextjs-react'
446
+
447
+ for root, dirs, files in os.walk(scan_root):
448
+ rp = Path(root)
449
+ dirs[:] = sorted(d for d in dirs if d not in skip_dirs)
450
+
451
+ # 检查目录名
452
+ if rp != scan_root:
453
+ dn = rp.name
454
+ rel = str(rp.relative_to(project_root))
455
+ if framework in ('nestjs', 'angular'):
456
+ all_violations.extend(check_nestjs_dir(dn, rel))
457
+ elif framework in ('nextjs-react', 'react', 'vue'):
458
+ all_violations.extend(check_frontend_dir(dn, rel))
459
+
460
+ # 检查文件名
461
+ for f in sorted(files):
462
+ if f in DEFAULT_SKIP_FILES:
463
+ continue
464
+ fp = rp / f
465
+ rel = str(fp.relative_to(project_root))
466
+ in_app = has_nextjs and str(rp).startswith(str(app_dir_path))
467
+ in_ui = _is_in_ui_dir(rel, root_rel, ui_dirs)
468
+
469
+ if framework == 'nestjs':
470
+ all_violations.extend(check_nestjs_file(f, rel, ok_files))
471
+ elif framework == 'angular':
472
+ all_violations.extend(check_angular_file(f, rel, ok_files))
473
+ elif framework in ('nextjs-react', 'react'):
474
+ if f.endswith('.tsx'):
475
+ all_violations.extend(
476
+ check_tsx_file(f, rel, ok_files, in_app, has_nextjs, in_ui))
477
+ elif f.endswith('.ts'):
478
+ all_violations.extend(check_ts_file(f, rel, ok_files))
479
+ elif framework == 'vue':
480
+ if f.endswith('.vue'):
481
+ all_violations.extend(check_vue_file(f, rel, ok_files))
482
+ elif f.endswith('.ts'):
483
+ all_violations.extend(check_ts_file(f, rel, ok_files))
484
+ elif framework == 'generic-ts':
485
+ all_violations.extend(check_generic_ts(f, rel, ok_files))
486
+
487
+ # 只保留 warning
488
+ filtered = [v for v in all_violations if v.severity == 'warning']
489
+
490
+ by_rule: dict[str, int] = {}
491
+ for v in filtered:
492
+ by_rule[v.rule] = by_rule.get(v.rule, 0) + 1
493
+
494
+ # 修复计划
495
+ fix_plan: list[dict] = []
496
+ for v in filtered:
497
+ if not v.suggested:
498
+ continue
499
+ if v.kind == 'file':
500
+ old = v.path
501
+ new = str(Path(v.path).parent / v.suggested)
502
+ fix_plan.append({'old': old, 'new': new,
503
+ 'git_mv': f'git mv "{old}" "{new}"'})
504
+ elif v.kind == 'directory':
505
+ old = v.path
506
+ parent = v.path.rsplit('/', 1)[0] if '/' in v.path else '.'
507
+ new = parent + '/' + v.suggested
508
+ fix_plan.append({'old': old, 'new': new,
509
+ 'note': '目录重命名需同步更新所有导入路径'})
510
+
511
+ return {
512
+ 'total_violations': len(filtered),
513
+ 'summary_by_rule': by_rule,
514
+ 'violations': [asdict(v) for v in filtered],
515
+ 'fix_plan': fix_plan,
516
+ }
517
+
518
+
519
+ def main() -> None:
520
+ # 从 stdin 或文件参数读取配置
521
+ if len(sys.argv) > 1 and sys.argv[1] != '-':
522
+ config_text = Path(sys.argv[1]).read_text()
523
+ else:
524
+ config_text = sys.stdin.read()
525
+
526
+ config = json.loads(config_text)
527
+ result = scan(config)
528
+ json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
529
+ print()
530
+ sys.exit(1 if result['total_violations'] > 0 else 0)
531
+
532
+
533
+ if __name__ == '__main__':
534
+ main()
@@ -1,37 +0,0 @@
1
- model = "gpt-5.3-codex"
2
- model_reasoning_effort = "medium"
3
- approval_policy = "never"
4
- sandbox_mode = "danger-full-access"
5
-
6
- developer_instructions = '''
7
- 你是 fix 代理。
8
-
9
- 输入必须包含:PR 编号、round、runId、fixFile。
10
- 缓存目录固定为 `./.cache/`,路径一律使用 `./.cache/<file>`(禁止 basename-only)。
11
-
12
- 快速失败(缺失即报错并退出):
13
- - 缺 PR 编号:`{"error":"MISSING_PR_NUMBER"}`
14
- - 缺 fixFile:`{"error":"MISSING_FIX_FILE"}`
15
- - fixFile 不可读:`{"error":"FIX_FILE_NOT_READABLE"}`
16
- - fixFile 无法解析 `IssuesToFix`:`{"error":"INVALID_FIX_FILE"}`
17
-
18
- 强制规则:
19
- 1. 仅处理 fixFile 中问题:`IssuesToFix` 必修;`OptionalIssues` 可修可拒绝(需写 reason),禁止范围外改动。
20
- 2. 每个 findingId 单独一个 commit,提交信息必须包含 findingId(示例:`fix(pr #<PR>): <FINDING_ID> <title>`)。
21
- 3. 每次提交后 push(首次无 upstream 可 `git push -u origin HEAD`),全部处理完再 `git push` 兜底。
22
- 4. 无法修复的项必须写明 reason,并记入 Rejected。
23
- 5. 必须维护 `./.cache/decision-log-pr<PR>.md`:按轮次追加 Fixed/Rejected,禁止覆盖历史。
24
- 6. Decision Log 每条必须包含:id、file、essence;预检修复允许 `file: __precheck__`。
25
- 7. 修复后执行 `dx lint` 与 `dx build all`;失败则不得声称完成。
26
- 8. 生成 `./.cache/fix-report-pr<PR>-r<ROUND>-<RUN_ID>.md`(可直接用于 PR 评论,且不包含本地绝对路径)。
27
- 9. 禁止执行 GitHub 评论发布动作(如 `gh pr comment` / `gh pr review`),发布由编排器负责。
28
- 10. 修改 `json/jsonc` 文件时必须使用脚本方式(如 python)改写,禁止手工字符串拼接导致格式损坏。
29
- 11. 最终响应只输出一行:`fixReportFile: ./.cache/<file>.md`;失败只输出一行 JSON 错误。
30
-
31
- 推送失败处理(强制):
32
- 1. 若 `git push` 失败,先判断是否为网络类失败(如 DNS 解析失败、连接拒绝、超时)。
33
- 2. 网络类失败必须重试当前 push 最多 2 次(总 3 次尝试),退避 2s / 5s。
34
- 3. 每次重试前执行轻量检查:`gh auth status`、`git remote -v`、`git ls-remote origin -h`。
35
- 4. 若重试后仍失败,必须返回结构化错误:`{"error":"GIT_PUSH_FAILED_NETWORK","step":"push","detail":"..."}`。
36
- 5. 禁止在 push 未成功时伪造“已完成修复”或输出成功 fixReportFile。
37
- '''
@@ -1,11 +0,0 @@
1
- model = "gpt-5.3-codex"
2
- model_reasoning_effort = "low"
3
- approval_policy = "never"
4
- sandbox_mode = "workspace-write"
5
-
6
- [sandbox_workspace_write]
7
- network_access = true
8
-
9
- developer_instructions = '''
10
- 你是 orchestrator 代理,负责协调 fixer 和 reviewer 以完成 PR 的修复与评审循环。
11
- '''