UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

255 lines (254 loc) 12.4 kB
/** * UnifiedValidator.js — 统一验证链 * * 替代 CandidateGuardrail + RecipeReadinessChecker 的分裂验证, * 提供单一入口的三层验证 (字段完整性 + 内容质量 + 去重)。 * * 统一严格模式:完整 REQUIRED 字段检查,无宽松降级。 * * @module shared/UnifiedValidator */ import { LanguageService } from '#shared/LanguageService.js'; import { FieldLevel, STANDARD_CATEGORIES, V3_FIELD_SPEC, VALID_KINDS, WHITELISTED_CATEGORIES, } from './FieldSpec.js'; // ── 代码指纹工具函数 ─────────────────────────────────────── /** 生成代码模式指纹 — 去除空白/注释后取前 200 字符的小写形式 */ function codeFingerprint(code) { return (code || '') .replace(/\/\/[^\n]*/g, '') // 移除单行注释 .replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释 .replace(/[\s]+/g, '') // 移除所有空白 .toLowerCase() .slice(0, 200); } // ── UnifiedValidator ──────────────────────────────────────── export class UnifiedValidator { /** 已提交标题 (小写) */ #titles; /** 已提交代码指纹 */ #codeFingerprints; /** * @param [options.existingTitles] 预填充已有标题 * @param [options.existingFingerprints] 预填充已有代码指纹 */ constructor(options = {}) { this.#titles = options.existingTitles || new Set(); this.#codeFingerprints = options.existingFingerprints || new Set(); } /** * 完整验证链 (3 层) * * @param candidate 候选数据(扁平字段) * @param [options.mode] 验证模式(自动检测或手动指定) * @param [options.systemInjectedFields] 系统注入的字段(跳过 REQUIRED 检查) * @param [options.skipUniqueness=false] 跳过去重检查 * @returns } */ validate(candidate, options = {}) { const errors = []; const warnings = []; const systemInjected = new Set(options.systemInjectedFields || []); // ── Layer 1: 字段完整性 (基于 V3_FIELD_SPEC) ── this.#checkFields(candidate, systemInjected, errors, warnings); // ── Layer 2: 内容质量 (来自 CandidateGuardrail.validateQuality) ── this.#checkContentQuality(candidate, errors, warnings); // ── Layer 3: 唯一性 (来自 CandidateGuardrail.validateUniqueness) ── if (!options.skipUniqueness) { this.#checkUniqueness(candidate, errors); } return { pass: errors.length === 0, errors, warnings, }; } // ── Layer 1: 基于 FieldSpec 检查 ───────────────────────── #checkFields(candidate, systemInjected, errors, warnings) { for (const field of V3_FIELD_SPEC) { const { name, level, rule } = field; // 系统注入字段:跳过 if (systemInjected.has(name)) { continue; } const value = this.#getNestedValue(candidate, name); const missing = this.#isMissing(value, field); if (!missing) { continue; } if (level === FieldLevel.REQUIRED) { errors.push(`缺少必填字段: ${name}${rule}`); } else if (level === FieldLevel.EXPECTED) { warnings.push(`建议填写: ${name}${rule}`); } // OPTIONAL: 不报任何问题 } // ── 额外的格式/值校验 ── // content 必须是对象 if (candidate.content && typeof candidate.content !== 'object') { errors.push('⚠️ content 必须是 JSON 对象(不是字符串!)。正确格式: { "markdown": "...", "rationale": "..." }'); } // reasoning 必须是对象 if (candidate.reasoning && typeof candidate.reasoning !== 'object') { errors.push('⚠️ reasoning 必须是 JSON 对象(不是字符串!)。正确格式: { "whyStandard": "...", "sources": [...], "confidence": 0.85 }'); } // kind 值校验 if (candidate.kind && !VALID_KINDS.includes(candidate.kind)) { errors.push(`kind 值无效: "${candidate.kind}" — 取值 rule/pattern/fact`); } // trigger 格式校验 if (candidate.trigger && !candidate.trigger.startsWith('@')) { warnings.push(`trigger "${candidate.trigger}" 应以 @ 开头`); } // category 值校验 if (candidate.category && !STANDARD_CATEGORIES.includes(candidate.category) && !WHITELISTED_CATEGORIES.includes(candidate.category)) { warnings.push(`category "${candidate.category}" 非标准值,应为: ${STANDARD_CATEGORIES.join('/')}(bootstrap/knowledge 等特殊来源可忽略此建议)`); } // language 校验 const lang = candidate.language?.toLowerCase(); if (lang && !LanguageService.isKnownLang(lang) && lang !== 'objc' && lang !== 'markdown') { warnings.push(`language "${candidate.language}" — 请使用标准语言标识 (swift/typescript/python/java/kotlin 等)`); } } // ── Layer 2: 内容质量启发式 ────────────────────────────── #checkContentQuality(candidate, errors, warnings) { const markdown = candidate.content?.markdown || ''; // markdown ≥ 200 字符 if (markdown && markdown.length > 0 && markdown.length < 200) { errors.push(`content.markdown 过短 (${markdown.length} 字符, 最少 200)。请包含代码片段和项目上下文描述。`); } // 代码块存在性 if (markdown && markdown.length >= 200 && !/```[\s\S]*?```/.test(markdown) && !/\.\w{1,10}(:\d+)?/.test(markdown)) { errors.push('content.markdown 中必须包含至少一个代码块或文件引用'); } // 来源引用(建议) if (markdown && markdown.length >= 200) { const hasSourceRef = /来源[::]|[Ss]ource[::]|\(\w+\.\w+:\d+\)/.test(markdown) || /[A-Z]\w+\.(?:m|h|swift|java|kt|js|ts|go|py|rs|rb|cs|cpp|c)/.test(markdown); if (!hasSourceRef) { warnings.push('建议在内容中标注代码来源 (来源: FileName.ext:行号)'); } // 源码位置质量检查 — 优先使用完整相对路径 const hasFullPathRef = /来源[::]\s*\S+\/\S+\.\w+:\d+/.test(markdown) || /\(\S+\/\S+\.\w+:\d+\)/.test(markdown); const hasBareName = /来源[::]\s*[A-Z]\w+\.\w+:\d+/.test(markdown) || /\([A-Z]\w+\.\w+:\d+\)/.test(markdown); if (hasBareName && !hasFullPathRef) { warnings.push('源码位置应使用完整相对路径+行号(如 Packages/ModuleName/Sources/.../FileName.swift:42),而非仅文件名'); } } // coreCode 语法完整性 { const coreCode = (candidate.coreCode || '').trim(); if (coreCode) { const firstChar = coreCode[0]; if (firstChar === '}' || firstChar === ')' || firstChar === ']') { errors.push(`coreCode 以 "${firstChar}" 开头 — 代码片段不完整,请包含完整的函数/方法/表达式`); } } } // 通用知识检测 const genericPatterns = [/^(Singleton|Factory|Observer|MVC|MVVM) (pattern|模式)$/i]; const title = candidate.title || ''; if (genericPatterns.some((p) => p.test(title.trim()))) { errors.push(`标题过于通用: "${title}" — 请加上项目特定的上下文`); } // 内容过于简单 if (markdown && markdown.length > 0 && markdown.length >= 200) { const lines = markdown.split('\n').filter((l) => l.trim().length > 0); if (lines.length <= 2 && !/```[\s\S]*?```/.test(markdown)) { warnings.push(`内容仅 ${lines.length} 行 — 建议包含更多代码片段和设计意图描述`); } } // reasoning.sources 路径质量检查 — 应包含路径分隔符,而非仅类名/文件名 const reasoning = candidate.reasoning; const sources = reasoning?.sources; if (Array.isArray(sources) && sources.length > 0) { const bareSources = sources.filter((s) => typeof s === 'string' && !s.includes('/') && !s.includes('\\')); if (bareSources.length > 0 && bareSources.length === sources.length) { warnings.push(`reasoning.sources 中的路径缺少目录结构(如 "${bareSources[0]}")— 应使用完整相对路径(如 Packages/ModuleName/Sources/.../FileName.swift)`); } } } // ── Layer 3: 去重 ──────────────────────────────────────── #checkUniqueness(candidate, errors) { const title = (candidate.title || '').toLowerCase().trim(); if (title && this.#titles.has(title)) { errors.push(`标题重复: "${candidate.title}"`); } const pattern = (candidate.content?.pattern || '').trim(); if (pattern.length >= 30) { const fp = codeFingerprint(pattern); if (fp.length >= 20 && this.#codeFingerprints.has(fp)) { errors.push('代码模式重复 — 已存在相同核心代码的候选。请提交不同的代码片段。'); } } } // ── 提交记录 ───────────────────────────────────────────── /** * 记录已提交的标题和代码指纹(提交成功后调用) * @param [pattern] 代码模式 */ recordSubmission(title, pattern) { if (title && typeof title === 'string') { this.#titles.add(title.toLowerCase().trim()); } if (pattern && typeof pattern === 'string' && pattern.length >= 30) { const fp = codeFingerprint(pattern); if (fp.length >= 20) { this.#codeFingerprints.add(fp); } } } // ── 工具函数 ───────────────────────────────────────────── /** * 获取嵌套字段值 * @param path 如 'content.markdown' 或 'reasoning.sources' */ #getNestedValue(obj, path) { const parts = path.split('.'); let current = obj; for (const part of parts) { if (current == null || typeof current !== 'object') { return undefined; } current = current[part]; } return current; } /** 检查值是否为"缺失" */ #isMissing(value, field) { if (value === undefined || value === null) { return true; } if (field.type === 'string') { return typeof value !== 'string' || !value.trim(); } if (field.type === 'array') { if (!Array.isArray(value)) { return true; } // reasoning.sources 必须非空 if (field.name === 'reasoning.sources') { return value.length === 0; } // headers 允许空数组 if (field.name === 'headers') { return false; } return false; } if (field.type === 'object') { return typeof value !== 'object'; } return !value; } } // ── 便捷工厂函数 ──────────────────────────────────────────── /** 创建一个无状态验证器实例(不含去重缓存),适用于一次性校验 */ export function createStatelessValidator() { return new UnifiedValidator(); } export default UnifiedValidator;