autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
185 lines (184 loc) • 6.99 kB
JavaScript
import Logger from '../../infrastructure/logging/Logger.js';
import { ConstitutionViolation } from '../../shared/errors/BaseError.js';
/**
* ConstitutionValidator — 数据守护验证器
*
* 精简设计: 4 条纯数据完整性规则,不做伦理/价值观判断。
* 每条规则对应 constitution.yaml 中的一个 rule.check 值。
*/
export class ConstitutionValidator {
checkers;
constitution;
logger;
constructor(constitution) {
this.constitution = constitution;
this.logger = Logger.getInstance();
/** rule.check → 检查函数 */
this.checkers = {
destructive_needs_confirmation: this._checkDestructive.bind(this),
creation_needs_content: this._checkContent.bind(this),
ai_cannot_approve_recipe: this._checkAiRecipe.bind(this),
batch_needs_authorization: this._checkBatch.bind(this),
};
}
/** 验证操作,返回违规列表 */
async validate(request) {
const violations = [];
const rules = this.constitution.getRules?.() || this.constitution.rules || [];
for (const rule of rules) {
const checker = this.checkers[rule.check];
if (checker) {
const v = checker(request, rule);
if (v) {
violations.push(v);
}
}
}
// 兼容旧 priorities 格式(如果新 rules 为空但有旧 priorities)
if (rules.length === 0) {
const priorities = this.constitution.getPriorities?.() || [];
for (const p of priorities) {
const pvs = await this._checkLegacyPriority(p, request);
violations.push(...pvs);
}
}
if (violations.length > 0) {
this.logger.warn('Constitution violations', {
actor: request.actor,
action: request.action,
count: violations.length,
});
}
return { compliant: violations.length === 0, violations };
}
/** 强制验证(违规时抛异常) */
async enforce(request) {
const result = await this.validate(request);
if (!result.compliant) {
throw new ConstitutionViolation(result.violations);
}
return result;
}
// ─── 规则检查器 ────────────────────────────────────────
/** 删除操作需要确认 */
_checkDestructive(req, rule) {
const destructive = ['delete', 'remove', 'destroy', 'purge', 'batch_delete'];
if (!destructive.some((w) => req.action?.toLowerCase().includes(w))) {
return null;
}
if (req.data?.confirmed || req.confirmed) {
return null;
}
return { rule: rule.id, reason: '操作未经确认', suggestion: '添加 confirmed: true' };
}
/** 创建候选/Recipe 需要内容 */
_checkContent(req, rule) {
const verb = this._verb(req.action);
const res = this._resource(req.action, req.resource);
const isCreation = (verb === 'create' && (res.includes('candidate') || res.includes('recipe'))) ||
req.action === 'create_candidate' ||
req.action === 'create_recipe';
if (!isCreation) {
return null;
}
const ok = req.data?.code ||
req.code ||
req.data?.content ||
(Array.isArray(req.data?.items) && req.data.items.length > 0) ||
req.data?.filePaths;
if (ok) {
return null;
}
return { rule: rule.id, reason: '缺少 code/content', suggestion: '提供代码或内容' };
}
/** AI 不能直接创建/批准 Recipe */
_checkAiRecipe(req, rule) {
if (!this._isAI(req.actor)) {
return null;
}
const verb = this._verb(req.action);
const res = this._resource(req.action, req.resource);
const isRecipeMod = (verb === 'create' && res.includes('recipe')) ||
verb === 'approve' ||
verb === 'publish' ||
req.action === 'approve_candidate' ||
req.action === 'create_recipe';
if (!isRecipeMod) {
return null;
}
return {
rule: rule.id,
reason: 'AI 不能直接操作 Recipe',
suggestion: '通过 Dashboard 人工审批',
};
}
/** 批量操作需要授权 */
_checkBatch(req, rule) {
if (!req.action?.includes('batch_')) {
return null;
}
if (req.data?.authorized || req.authorized) {
return null;
}
return { rule: rule.id, reason: '缺少授权标志', suggestion: '添加 authorized: true' };
}
// ─── 辅助方法 ──────────────────────────────────────────
_verb(action) {
if (!action) {
return '';
}
return action.includes(':') ? (action.split(':').pop() ?? '') : action;
}
_resource(action, resource) {
if (action?.includes(':')) {
return action.split(':')[0];
}
if (typeof resource === 'string' && resource.startsWith('/')) {
const m = resource.match(/^\/([^/]+)/);
return m ? m[1] : resource;
}
return resource || '';
}
_isAI(actor) {
return ['external_agent', 'chat_agent'].some((a) => actor?.toLowerCase().includes(a));
}
// ─── 旧格式兼容 ───────────────────────────────────────
/** 兼容旧 priorities 格式 */
async _checkLegacyPriority(priority, request) {
const violations = [];
if (priority.id === 1) {
const v = this._checkDestructive(request, {
id: 'destructive_confirm',
check: 'destructive_needs_confirmation',
});
if (v) {
violations.push(v);
}
const v2 = this._checkContent(request, {
id: 'content_required',
check: 'creation_needs_content',
});
if (v2) {
violations.push(v2);
}
}
if (priority.id === 2) {
const v = this._checkAiRecipe(request, {
id: 'ai_no_direct_recipe',
check: 'ai_cannot_approve_recipe',
});
if (v) {
violations.push(v);
}
const v2 = this._checkBatch(request, {
id: 'batch_authorized',
check: 'batch_needs_authorization',
});
if (v2) {
violations.push(v2);
}
}
return violations;
}
}
export default ConstitutionValidator;