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

6.2 KiB
Raw Blame History

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

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 依赖,可替换 parseYamlSimpleyaml.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 通过