Files
2026Technology-Competition/docs/superpowers/specs/implementation-steps/11-phase4.3-custom-rules.md
T

224 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Step 11 — Phase 4.3: 自定义规则系统
**依赖**: Step 01(配置模块)
**参考设计**: §12
## 目标
实现 YAML 规则加载器,从 `.code-review/rules/*.yaml` 读取规则定义,按 `config.yaml` 过滤启用状态。
## 新建文件
| # | 文件 | 说明 |
|---|------|------|
| 1 | `src/rules/yaml-parser.ts` | `loadActiveRules()` + `parseConfigYaml()` |
## 现有参考文件(已存在,不修改)
```
.code-review/
├── config.yaml # 规则启用配置
└── rules/
├── security-rules.yaml # 安全规则
└── coding-conventions.yaml # 编码规范
```
---
## `src/rules/yaml-parser.ts`
```typescript
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
export interface CustomRule {
id: string;
severity: 'error' | 'warning' | 'info';
description: string;
message: string;
languages?: string[];
}
interface RuleYamlItem {
id: string;
severity: string;
description: string;
message: string;
languages?: string[];
}
interface RuleConfig {
enabled?: string[];
rules?: Record<string, { enabled: boolean }>;
}
function parseYamlSimple(content: string): object[] {
const items: Array<Record<string, unknown>> = [];
let current: Record<string, unknown> | null = null;
let currentKey = '';
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) { continue; }
if (trimmed.startsWith('- ')) {
if (current) { items.push(current); }
current = {};
const indentMatch = trimmed.match(/^- (\w[\w-]*)\s*:\s*(.*)$/);
if (indentMatch) {
const key = indentMatch[1];
let value: unknown = indentMatch[2].trim();
if (value.startsWith('[') && value.endsWith(']')) {
value = value.slice(1, -1).split(',').map(s =>
s.trim().replace(/^['"]|['"]$/g, '')
);
}
current[key] = value;
}
} else if (current) {
const propMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
if (propMatch) {
const key = propMatch[1];
let value: unknown = propMatch[2].trim();
if (!value || value === '[]') {
value = [];
} else if (value.startsWith('[') && value.endsWith(']')) {
value = value.slice(1, -1).split(',').map(s =>
s.trim().replace(/^['"]|['"]$/g, '')
);
}
current[key] = value;
}
}
}
if (current) { items.push(current); }
return items;
}
function parseConfigYaml(content: string): RuleConfig {
const config: RuleConfig = { enabled: [], rules: {} };
let section: string | null = null;
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) { continue; }
if (trimmed === 'enabled:') {
section = 'enabled';
continue;
}
if (trimmed === 'rules:') {
section = 'rules';
continue;
}
if (section === 'enabled' && trimmed.startsWith('- ')) {
const name = trimmed.substring(2).trim();
if (!config.enabled) { config.enabled = []; }
config.enabled!.push(name);
}
if (section === 'rules') {
const ruleMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*$/);
if (ruleMatch) {
currentKey = ruleMatch[1];
if (!config.rules) { config.rules = {}; }
config.rules[currentKey] = { enabled: true };
} else if (currentKey) {
const propMatch = trimmed.match(/^(\w+)\s*:\s*(.*)$/);
if (propMatch) {
const key = propMatch[1];
const value = propMatch[2].trim();
if (!config.rules) { config.rules = {}; }
if (!config.rules[currentKey]) { config.rules[currentKey] = { enabled: true }; }
(config.rules[currentKey] as Record<string, unknown>)[key] =
value === 'false' ? false : value === 'true' ? true : value;
}
}
}
}
return config;
}
export function loadActiveRules(workspaceRoot: string): CustomRule[] {
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
if (!fs.existsSync(rulesDir)) { return []; }
let ruleConfig: RuleConfig = {};
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, 'utf-8');
ruleConfig = parseConfigYaml(configContent);
}
const enabledFiles = new Set(ruleConfig.enabled ?? []);
const disabledRules = new Set(
Object.entries(ruleConfig.rules ?? {})
.filter(([, v]) => v.enabled === false)
.map(([k]) => k)
);
const allRules: CustomRule[] = [];
const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
for (const file of files) {
if (enabledFiles.size > 0 && !enabledFiles.has(file)) { continue; }
const content = fs.readFileSync(path.join(rulesDir, file), 'utf-8');
const items = parseYamlSimple(content) as RuleYamlItem[];
for (const item of items) {
if (disabledRules.has(item.id)) { continue; }
if (!item.id || !item.severity || !item.description || !item.message) { continue; }
const severity = ['error', 'warning', 'info'].includes(item.severity)
? (item.severity as 'error' | 'warning' | 'info')
: 'warning';
allRules.push({
id: item.id,
severity,
description: item.description,
message: item.message,
languages: item.languages,
});
}
}
return allRules;
}
```
> **注意**: 如果项目已安装 `js-yaml` 依赖,可替换 `parseYamlSimple` 为 `yaml.load()`。此处用简易解析器避免额外依赖。
---
## 关键逻辑
**加载流程**(按设计 §12.3:
```
1. 扫描 .code-review/rules/*.yaml → 加载所有规则定义
2. 读取 .code-review/config.yaml
3. 按文件级 enabled 列表过滤
4. 按规则级 rules.<id>.enabled 覆盖
5. 返回激活的规则列表
```
- `enabled`: 文件级启用列表,为空时默认全部启用
- `rules.<id>.enabled: false`: 单独禁用特定规则
- 规则 ID 不含 `custom:` 前缀,运行时由 AI 引擎自动拼接
- 规则各字段与设计 §12.2 一致
---
## 验收
- [ ] 文件创建完成
- [ ] `npm run compile` 通过
- [ ] `npm run lint` 通过