@ranger1/dx 0.1.90 → 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.
- package/README.md +2 -2
- package/lib/cli/commands/core.js +92 -0
- package/lib/cli/help.js +2 -1
- package/lib/codex-initial.js +19 -215
- 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
|
@@ -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()
|
package/codex/agents/fixer.toml
DELETED
|
@@ -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
|
-
'''
|