autosnippet 2.1.0 → 2.5.0

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.
Files changed (50) hide show
  1. package/README.md +189 -113
  2. package/bin/api-server.js +1 -4
  3. package/bin/cli.js +1 -50
  4. package/config/constitution.yaml +33 -107
  5. package/dashboard/dist/assets/{icons-B5rs8uNb.js → icons-Dtm0E6DS.js} +95 -85
  6. package/dashboard/dist/assets/index-B7VpZOCz.css +1 -0
  7. package/dashboard/dist/assets/index-D87IZTmZ.js +187 -0
  8. package/dashboard/dist/assets/{react-markdown-Bp8u1wRC.js → react-markdown-CWxUbOf4.js} +1 -1
  9. package/dashboard/dist/assets/{syntax-highlighter-C6bvFtpx.js → syntax-highlighter-CJ2drQQb.js} +1 -1
  10. package/dashboard/dist/assets/{vendor-Cky7Jynh.js → vendor-f83ah6cm.js} +13 -13
  11. package/dashboard/dist/index.html +6 -6
  12. package/lib/bootstrap.js +5 -31
  13. package/lib/cli/SetupService.js +46 -18
  14. package/lib/core/capability/CapabilityProbe.js +8 -6
  15. package/lib/core/constitution/Constitution.js +13 -4
  16. package/lib/core/constitution/ConstitutionValidator.js +106 -211
  17. package/lib/core/gateway/Gateway.js +34 -98
  18. package/lib/core/gateway/GatewayActionRegistry.js +12 -1
  19. package/lib/core/permission/PermissionManager.js +2 -2
  20. package/lib/external/ai/AiProvider.js +47 -7
  21. package/lib/external/mcp/McpServer.js +4 -7
  22. package/lib/external/mcp/handlers/bootstrap.js +13 -1
  23. package/lib/external/mcp/handlers/browse.js +0 -7
  24. package/lib/external/mcp/handlers/candidate.js +1 -1
  25. package/lib/external/mcp/handlers/guard.js +11 -0
  26. package/lib/external/mcp/handlers/skill.js +186 -18
  27. package/lib/external/mcp/tools.js +40 -1
  28. package/lib/http/HttpServer.js +4 -0
  29. package/lib/http/middleware/roleResolver.js +1 -1
  30. package/lib/http/routes/auth.js +2 -2
  31. package/lib/http/routes/monitoring.js +4 -4
  32. package/lib/http/routes/search.js +0 -17
  33. package/lib/http/routes/skills.js +73 -0
  34. package/lib/injection/ServiceContainer.js +21 -40
  35. package/lib/service/candidate/CandidateService.js +12 -1
  36. package/lib/service/chat/ChatAgent.js +139 -18
  37. package/lib/service/chat/Memory.js +104 -0
  38. package/lib/service/chat/tools.js +244 -10
  39. package/lib/service/guard/GuardCheckEngine.js +9 -1
  40. package/lib/service/recipe/RecipeService.js +8 -0
  41. package/lib/service/skills/SkillHooks.js +126 -0
  42. package/package.json +1 -1
  43. package/scripts/init-db.js +1 -2
  44. package/templates/constitution.yaml +29 -85
  45. package/dashboard/dist/assets/index-0YzLw2ga.css +0 -1
  46. package/dashboard/dist/assets/index-DbkbX1c-.js +0 -154
  47. package/lib/core/session/SessionManager.js +0 -232
  48. package/lib/infrastructure/logging/ReasoningLogger.js +0 -269
  49. package/lib/infrastructure/monitoring/RoleDriftMonitor.js +0 -259
  50. package/lib/infrastructure/quality/ComplianceEvaluator.js +0 -326
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>AutoSnippet Dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-DbkbX1c-.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-D87IZTmZ.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-Cky7Jynh.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-f83ah6cm.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
12
- <link rel="modulepreload" crossorigin href="/assets/icons-B5rs8uNb.js">
13
- <link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-C6bvFtpx.js">
14
- <link rel="modulepreload" crossorigin href="/assets/react-markdown-Bp8u1wRC.js">
15
- <link rel="stylesheet" crossorigin href="/assets/index-0YzLw2ga.css">
12
+ <link rel="modulepreload" crossorigin href="/assets/icons-Dtm0E6DS.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-CJ2drQQb.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/react-markdown-CWxUbOf4.js">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-B7VpZOCz.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
package/lib/bootstrap.js CHANGED
@@ -8,12 +8,9 @@ import Constitution from './core/constitution/Constitution.js';
8
8
  import ConstitutionValidator from './core/constitution/ConstitutionValidator.js';
9
9
  import PermissionManager from './core/permission/PermissionManager.js';
10
10
  import Gateway from './core/gateway/Gateway.js';
11
- import SessionManager from './core/session/SessionManager.js';
12
11
  import AuditLogger from './infrastructure/audit/AuditLogger.js';
13
12
  import AuditStore from './infrastructure/audit/AuditStore.js';
14
- import { initReasoningLogger } from './infrastructure/logging/ReasoningLogger.js';
15
- import { initRoleDriftMonitor, createRoleDriftPlugin } from './infrastructure/monitoring/RoleDriftMonitor.js';
16
- import { initComplianceEvaluator } from './infrastructure/quality/ComplianceEvaluator.js';
13
+ import { SkillHooks } from './service/skills/SkillHooks.js';
17
14
 
18
15
  const __filename = fileURLToPath(import.meta.url);
19
16
  const __dirname = path.dirname(__filename);
@@ -148,11 +145,6 @@ export class Bootstrap {
148
145
  this.components.permissionManager = permissionManager;
149
146
  logger.info('PermissionManager initialized');
150
147
 
151
- // Session Manager
152
- const sessionManager = new SessionManager(db);
153
- this.components.sessionManager = sessionManager;
154
- logger.info('SessionManager initialized');
155
-
156
148
  // Audit System
157
149
  const auditStore = new AuditStore(db);
158
150
  const auditLogger = new AuditLogger(auditStore);
@@ -160,20 +152,10 @@ export class Bootstrap {
160
152
  this.components.auditLogger = auditLogger;
161
153
  logger.info('Audit system initialized');
162
154
 
163
- // Reasoning Logger (透明推理日志)
164
- const reasoningLogger = initReasoningLogger(db);
165
- this.components.reasoningLogger = reasoningLogger;
166
- logger.info('ReasoningLogger initialized');
167
-
168
- // Role Drift Monitor (角色漂移检测)
169
- const roleDriftMonitor = initRoleDriftMonitor(db);
170
- this.components.roleDriftMonitor = roleDriftMonitor;
171
- logger.info('RoleDriftMonitor initialized');
172
-
173
- // Compliance Evaluator (合规评估)
174
- const complianceEvaluator = initComplianceEvaluator(db);
175
- this.components.complianceEvaluator = complianceEvaluator;
176
- logger.info('ComplianceEvaluator initialized');
155
+ // Skill Hooks (扫描 skills/*/hooks.js + .autosnippet/skills/*/hooks.js)
156
+ const skillHooks = new SkillHooks();
157
+ await skillHooks.load();
158
+ this.components.skillHooks = skillHooks;
177
159
  }
178
160
 
179
161
  /**
@@ -191,17 +173,9 @@ export class Bootstrap {
191
173
  constitutionValidator: this.components.constitutionValidator,
192
174
  permissionManager: this.components.permissionManager,
193
175
  auditLogger: this.components.auditLogger,
194
- sessionManager: this.components.sessionManager,
195
176
  });
196
177
 
197
178
  this.components.gateway = gateway;
198
-
199
- // 注册角色漂移检测插件
200
- if (this.components.roleDriftMonitor) {
201
- gateway.use(createRoleDriftPlugin(this.components.roleDriftMonitor));
202
- this.components.logger.info('RoleDrift plugin registered in Gateway');
203
- }
204
-
205
179
  this.components.logger.info('Gateway initialized');
206
180
  }
207
181
 
@@ -265,25 +265,27 @@ export class SetupService {
265
265
  ' no_remote: "allow"',
266
266
  ' cache_ttl: 86400',
267
267
  '',
268
- 'priorities:',
269
- ' - id: 1',
270
- ' name: "Data Integrity"',
271
- ' rules: ["删除操作必须有确认步骤"]',
272
- ' - id: 2',
273
- ' name: "Human Oversight"',
274
- ' rules: ["AI 生成的 Candidate 必须经人工审核"]',
268
+ 'rules:',
269
+ ' - id: destructive_confirm',
270
+ ' check: "删除操作必须有 confirmed: true"',
271
+ ' - id: content_required',
272
+ ' check: "创建 candidate/recipe 必须提供 code 或 content"',
273
+ ' - id: ai_no_direct_recipe',
274
+ ' check: "AI actor 不能直接创建或批准 Recipe"',
275
+ ' - id: batch_authorized',
276
+ ' check: "批量操作必须有 authorized: true"',
275
277
  '',
276
278
  'roles:',
277
- ' - id: "developer_admin"',
279
+ ' - id: "developer"',
280
+ ' name: "Developer"',
278
281
  ' permissions: ["*"]',
279
282
  ' requires_capability: ["git_write"]',
280
- ' - id: "developer_contributor"',
281
- ' permissions: ["read:*", "approve:candidates", "create:recipes"]',
282
- ' requires_capability: ["git_write"]',
283
- ' - id: "cursor_agent"',
283
+ ' - id: "external_agent"',
284
+ ' name: "External Agent"',
284
285
  ' permissions: ["read:recipes", "read:guard_rules", "create:candidates", "submit:candidates"]',
285
- ' - id: "visitor"',
286
- ' permissions: ["read:recipes", "read:candidates", "read:guard_rules", "search:query"]',
286
+ ' - id: "chat_agent"',
287
+ ' name: "ChatAgent"',
288
+ ' permissions: ["read:recipes", "read:candidates", "create:candidates", "read:guard_rules"]',
287
289
  '',
288
290
  ].join('\n'));
289
291
  }
@@ -621,11 +623,37 @@ export class SetupService {
621
623
  let content = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
622
624
  let changed = false;
623
625
 
624
- // ── 必须忽略:.autosnippet/(运行时缓存、DB、向量索引)──
625
- if (!content.includes('.autosnippet/')) {
626
- content += `\n# AutoSnippet 运行时缓存(不入库)\n.autosnippet/\n`;
626
+ // ── v2.4.0 迁移:旧格式 ".autosnippet/" → 新格式 ".autosnippet/*" ──
627
+ // 旧格式会忽略整个目录(git 不遍历内部),导致 skills/ 和 config.json 无法被 negation 恢复
628
+ // 新格式忽略目录内所有文件,允许 negation 模式取消特定子路径
629
+ if (content.includes('.autosnippet/') && !content.includes('.autosnippet/*')) {
630
+ content = content.replace(
631
+ /^\.autosnippet\/$/m,
632
+ '.autosnippet/*',
633
+ );
634
+ changed = true;
635
+ console.log(' ✅ .gitignore: .autosnippet/ → .autosnippet/*(升级为精细忽略)');
636
+ }
637
+
638
+ // ── 必须忽略:.autosnippet/*(运行时缓存、DB、向量索引、memory) ──
639
+ if (!content.includes('.autosnippet/') && !content.includes('.autosnippet/*')) {
640
+ content += `\n# AutoSnippet 运行时缓存(不入库)\n.autosnippet/*\n`;
641
+ changed = true;
642
+ console.log(' ✅ .gitignore += .autosnippet/*');
643
+ }
644
+
645
+ // ── 必须跟踪:.autosnippet/config.json(项目配置) ──
646
+ if (!content.includes('!.autosnippet/config.json')) {
647
+ content += `!.autosnippet/config.json\n`;
648
+ changed = true;
649
+ console.log(' ✅ .gitignore += !.autosnippet/config.json');
650
+ }
651
+
652
+ // ── 必须跟踪:.autosnippet/skills/(项目级 Skill 文档) ──
653
+ if (!content.includes('!.autosnippet/skills/')) {
654
+ content += `!.autosnippet/skills/\n`;
627
655
  changed = true;
628
- console.log(' ✅ .gitignore += .autosnippet/');
656
+ console.log(' ✅ .gitignore += !.autosnippet/skills/');
629
657
  }
630
658
 
631
659
  // ── 必须跟踪:AutoSnippet/(知识库子仓库)──
@@ -5,9 +5,9 @@
5
5
  * 探测结果被缓存(默认 24h)以避免重复执行。
6
6
  *
7
7
  * 三种探测结果:
8
- * 'admin' — 无子仓库(个人项目)/ 有 push 权限 → developer_admin
9
- * 'contributor' — 有子仓库但无 push 权限 → developer_contributor
10
- * 'visitor' — (仅 AUTH_ENABLED=true 且无效 token 时出现)
8
+ * 'admin' — 无子仓库(个人项目)/ 有 push 权限 → developer
9
+ * 'contributor' — 有子仓库但无 push 权限 → developer(本地用户 = 项目 Owner)
10
+ * 'visitor' — noRemote=deny 严格模式 developer(仅探针级别区分,角色统一为 developer)
11
11
  *
12
12
  * 当没有 remote 时根据 constitution capabilities.git_write.no_remote 策略决定:
13
13
  * 'allow' (默认) — 本地开发,视为 admin
@@ -78,11 +78,13 @@ export class CapabilityProbe {
78
78
  * @returns {string}
79
79
  */
80
80
  toRole(probeResult) {
81
+ // 本地运行 AutoSnippet 的用户 = 项目 Owner = developer
82
+ // 探针级别的 admin/contributor/visitor 仅做信息记录,角色统一为 developer
81
83
  switch (probeResult) {
82
- case 'admin': return 'developer_admin';
83
- case 'contributor': return 'developer_contributor';
84
+ case 'admin':
85
+ case 'contributor':
84
86
  case 'visitor':
85
- default: return 'visitor';
87
+ default: return 'developer';
86
88
  }
87
89
  }
88
90
 
@@ -10,6 +10,7 @@ export class Constitution {
10
10
  this.configPath = configPath;
11
11
  this.config = this.loadConfig();
12
12
  this.priorities = this.config.priorities || [];
13
+ this.rules = this.config.rules || [];
13
14
  this.roles = new Map(this.config.roles?.map((r) => [r.id, r]) || []);
14
15
  }
15
16
 
@@ -32,6 +33,13 @@ export class Constitution {
32
33
  return this.priorities;
33
34
  }
34
35
 
36
+ /**
37
+ * 获取所有数据守护规则
38
+ */
39
+ getRules() {
40
+ return this.rules;
41
+ }
42
+
35
43
  /**
36
44
  * 获取能力定义
37
45
  */
@@ -97,6 +105,7 @@ export class Constitution {
97
105
  reload() {
98
106
  this.config = this.loadConfig();
99
107
  this.priorities = this.config.priorities || [];
108
+ this.rules = this.config.rules || [];
100
109
  this.roles = new Map(this.config.roles?.map((r) => [r.id, r]) || []);
101
110
  }
102
111
 
@@ -107,10 +116,10 @@ export class Constitution {
107
116
  return {
108
117
  version: this.config.version,
109
118
  effectiveDate: this.config.effective_date,
110
- priorities: this.priorities.map((p) => ({
111
- id: p.id,
112
- name: p.name,
113
- description: p.description,
119
+ priorities: this.priorities,
120
+ rules: this.rules.map((r) => ({
121
+ id: r.id,
122
+ description: r.description,
114
123
  })),
115
124
  roles: Array.from(this.roles.values()).map((r) => ({
116
125
  id: r.id,
@@ -2,258 +2,153 @@ import { ConstitutionViolation } from '../../shared/errors/BaseError.js';
2
2
  import Logger from '../../infrastructure/logging/Logger.js';
3
3
 
4
4
  /**
5
- * ConstitutionValidator - 宪法验证器
5
+ * ConstitutionValidator 数据守护验证器
6
+ *
7
+ * 精简设计: 4 条纯数据完整性规则,不做伦理/价值观判断。
8
+ * 每条规则对应 constitution.yaml 中的一个 rule.check 值。
6
9
  */
7
10
  export class ConstitutionValidator {
8
11
  constructor(constitution) {
9
12
  this.constitution = constitution;
10
13
  this.logger = Logger.getInstance();
14
+
15
+ /** rule.check → 检查函数 */
16
+ this.checkers = {
17
+ destructive_needs_confirmation: this._checkDestructive.bind(this),
18
+ creation_needs_content: this._checkContent.bind(this),
19
+ ai_cannot_approve_recipe: this._checkAiRecipe.bind(this),
20
+ batch_needs_authorization: this._checkBatch.bind(this),
21
+ };
11
22
  }
12
23
 
13
24
  /**
14
- * 验证操作是否违反宪法
25
+ * 验证操作,返回违规列表
15
26
  */
16
27
  async validate(request) {
17
28
  const violations = [];
18
- const priorities = this.constitution.getPriorities();
29
+ const rules = this.constitution.getRules?.() || this.constitution.rules || [];
19
30
 
20
- for (const priority of priorities) {
21
- const priorityViolations = await this.checkPriority(priority, request);
22
- violations.push(...priorityViolations);
31
+ for (const rule of rules) {
32
+ const checker = this.checkers[rule.check];
33
+ if (checker) {
34
+ const v = checker(request, rule);
35
+ if (v) violations.push(v);
36
+ }
23
37
  }
24
38
 
25
- // 按优先级排序(优先级 1 最重要)
26
- violations.sort((a, b) => a.priority - b.priority);
27
-
28
- const result = {
29
- compliant: violations.length === 0,
30
- violations: violations,
31
- highestViolatedPriority: violations[0]?.priority || null,
32
- };
39
+ // 兼容旧 priorities 格式(如果新 rules 为空但有旧 priorities)
40
+ if (rules.length === 0) {
41
+ const priorities = this.constitution.getPriorities?.() || [];
42
+ for (const p of priorities) {
43
+ const pvs = await this._checkLegacyPriority(p, request);
44
+ violations.push(...pvs);
45
+ }
46
+ }
33
47
 
34
- if (!result.compliant) {
35
- this.logger.warn('Constitution violations detected', {
48
+ if (violations.length > 0) {
49
+ this.logger.warn('Constitution violations', {
36
50
  actor: request.actor,
37
51
  action: request.action,
38
- violations: result.violations.length,
52
+ count: violations.length,
39
53
  });
40
54
  }
41
55
 
42
- return result;
56
+ return { compliant: violations.length === 0, violations };
43
57
  }
44
58
 
45
59
  /**
46
- * 检查特定优先级的规则
60
+ * 强制验证(违规时抛异常)
47
61
  */
48
- async checkPriority(priority, request) {
49
- const violations = [];
50
- const verb = this._extractVerb(request.action);
51
- const resName = this._extractResourceName(request.action, request.resource);
52
-
53
- // Priority 1: Data Integrity
54
- if (priority.id === 1) {
55
- // 检查破坏性操作
56
- if (this.isDestructiveOperation(request)) {
57
- if (!request.data?.confirmed && !request.confirmed) {
58
- violations.push({
59
- priority: 1,
60
- rule: '删除操作必须有确认步骤',
61
- reason: '操作未经确认',
62
- suggestion: '添加 confirmed: true 或 --confirm 标志',
63
- });
64
- }
65
- }
66
-
67
- // 检查数据完整性
68
- // 支持 Gateway 格式 (candidate:create) + REST 格式 (create + /candidates) + Legacy 格式 (create_candidate)
69
- const isCandidateOrRecipeCreation =
70
- (verb === 'create' && (resName.includes('candidate') || resName.includes('recipe'))) ||
71
- (request.action === 'create' && (request.resource?.includes('/candidates') || request.resource?.includes('/recipes'))) ||
72
- request.action === 'create_candidate' ||
73
- request.action === 'create_recipe';
74
-
75
- if (isCandidateOrRecipeCreation) {
76
- // 单条: data.code / data.content
77
- // 批量: data.items (每条内含 code)
78
- // 草稿: data.filePaths (内容在文件中)
79
- const hasContent = request.data?.code || request.code || request.data?.content
80
- || (Array.isArray(request.data?.items) && request.data.items.length > 0)
81
- || request.data?.filePaths;
82
- if (!hasContent) {
83
- violations.push({
84
- priority: 1,
85
- rule: '所有内容必须可验证',
86
- reason: '缺少 code/content 字段',
87
- suggestion: '提供完整的代码或内容',
88
- });
89
- }
90
- }
91
- }
92
-
93
- // Priority 2: Human Oversight
94
- if (priority.id === 2) {
95
- // AI 不能直接批准或创建 Recipe
96
- // 支持 Gateway 格式 (recipe:create, candidate:approve, recipe:publish) + Legacy 格式
97
- const isRecipeModification =
98
- (verb === 'create' && resName.includes('recipe')) ||
99
- (verb === 'approve') ||
100
- (verb === 'publish' && resName.includes('recipe')) ||
101
- (request.action === 'create' && request.resource?.includes('/recipes')) ||
102
- request.action === 'approve_candidate' ||
103
- request.action === 'approve_recipe' ||
104
- request.action === 'create_recipe';
105
-
106
- if (isRecipeModification) {
107
- if (this.isAIActor(request.actor)) {
108
- violations.push({
109
- priority: 2,
110
- rule: 'AI 生成的 Candidate 必须经人工审核',
111
- reason: 'AI 不能直接批准或创建 Recipe',
112
- suggestion: '通过 Dashboard 人工审批或使用 developer_admin 角色',
113
- });
114
- }
115
- }
116
-
117
- // 批量操作需要授权
118
- if (request.action === 'batch_update' || request.action === 'batch_delete') {
119
- if (!request.data?.authorized && !request.authorized) {
120
- violations.push({
121
- priority: 2,
122
- rule: '批量操作需要明确授权',
123
- reason: '缺少授权标志',
124
- suggestion: '添加 authorized: true 标志',
125
- });
126
- }
127
- }
128
- }
129
-
130
- // Priority 3: AI Transparency
131
- if (priority.id === 3) {
132
- // AI 生成必须有 reasoning
133
- // 支持 Gateway 格式 (candidate:create) + REST 格式 + Legacy 格式
134
- const isCandidateCreation =
135
- (verb === 'create' && resName.includes('candidate')) ||
136
- (request.action === 'create' && request.resource?.includes('/candidates')) ||
137
- request.action === 'create_candidate';
138
-
139
- if (isCandidateCreation) {
140
- if (this.isAIActor(request.actor)) {
141
- // MCP handler (_createCandidateItem) 会自动生成默认 reasoning,
142
- // 此处仅在 reasoning 明确为空对象时警告,不阻断提交
143
- const r = request.data?.reasoning;
144
- if (r && typeof r === 'object') {
145
- if (!r.whyStandard && !r.sources) {
146
- violations.push({
147
- priority: 3,
148
- rule: 'Reasoning 信息必须完整',
149
- reason: '提供了 reasoning 但缺少 whyStandard 和 sources',
150
- suggestion: '提供完整的推理过程说明',
151
- });
152
- }
153
- }
154
- // 未传 reasoning 时不视为违规 — handler 层会自动生成默认值
155
- }
156
- }
157
-
158
- // Guard 规则(现在是 Recipe)必须有来源
159
- // 支持 Gateway 格式 (guard_rule:create) + REST 格式 + Legacy 格式
160
- const isGuardRuleModification =
161
- ((verb === 'create' || verb === 'update' || verb === 'enable') && resName.includes('guard')) ||
162
- (request.action === 'create' && request.resource?.includes('/guard')) ||
163
- (request.action === 'update' && request.resource?.includes('/guard')) ||
164
- request.action === 'create_guard_rule' ||
165
- request.action === 'update_guard_rule';
166
-
167
- if (isGuardRuleModification) {
168
- if (!request.data?.source_recipe_id && !request.data?.sourceCandidate) {
169
- violations.push({
170
- priority: 3,
171
- rule: 'Guard 规则必须关联来源',
172
- reason: '缺少 source_recipe_id 或 sourceCandidate',
173
- suggestion: '指定规则来源的 Recipe ID 或 Candidate ID',
174
- });
175
- }
176
- }
62
+ async enforce(request) {
63
+ const result = await this.validate(request);
64
+ if (!result.compliant) {
65
+ throw new ConstitutionViolation(result.violations);
177
66
  }
67
+ return result;
68
+ }
178
69
 
179
- // Priority 4: Helpfulness - 通常不会导致违规,仅提示优化建议
180
- // (此优先级主要用于质量评估,而非硬性限制)
70
+ // ─── 规则检查器 ────────────────────────────────────────
181
71
 
182
- return violations;
72
+ /** 删除操作需要确认 */
73
+ _checkDestructive(req, rule) {
74
+ const destructive = ['delete', 'remove', 'destroy', 'purge', 'batch_delete'];
75
+ if (!destructive.some(w => req.action?.toLowerCase().includes(w))) return null;
76
+ if (req.data?.confirmed || req.confirmed) return null;
77
+ return { rule: rule.id, reason: '操作未经确认', suggestion: '添加 confirmed: true' };
183
78
  }
184
79
 
185
- /**
186
- * 判断是否为破坏性操作
187
- */
188
- isDestructiveOperation(request) {
189
- const destructiveActions = [
190
- 'delete',
191
- 'remove',
192
- 'destroy',
193
- 'purge',
194
- 'truncate',
195
- 'drop',
196
- 'batch_delete',
197
- ];
80
+ /** 创建候选/Recipe 需要内容 */
81
+ _checkContent(req, rule) {
82
+ const verb = this._verb(req.action);
83
+ const res = this._resource(req.action, req.resource);
84
+ const isCreation =
85
+ (verb === 'create' && (res.includes('candidate') || res.includes('recipe'))) ||
86
+ req.action === 'create_candidate' || req.action === 'create_recipe';
87
+ if (!isCreation) return null;
88
+ const ok = req.data?.code || req.code || req.data?.content
89
+ || (Array.isArray(req.data?.items) && req.data.items.length > 0)
90
+ || req.data?.filePaths;
91
+ if (ok) return null;
92
+ return { rule: rule.id, reason: '缺少 code/content', suggestion: '提供代码或内容' };
93
+ }
198
94
 
199
- return destructiveActions.some((word) => request.action.toLowerCase().includes(word));
95
+ /** AI 不能直接创建/批准 Recipe */
96
+ _checkAiRecipe(req, rule) {
97
+ if (!this._isAI(req.actor)) return null;
98
+ const verb = this._verb(req.action);
99
+ const res = this._resource(req.action, req.resource);
100
+ const isRecipeMod =
101
+ (verb === 'create' && res.includes('recipe')) || verb === 'approve' || verb === 'publish'
102
+ || req.action === 'approve_candidate' || req.action === 'create_recipe';
103
+ if (!isRecipeMod) return null;
104
+ return { rule: rule.id, reason: 'AI 不能直接操作 Recipe', suggestion: '通过 Dashboard 人工审批' };
200
105
  }
201
106
 
202
- /**
203
- * 判断是否为 AI 角色
204
- */
205
- isAIActor(actor) {
206
- const aiActors = ['cursor_agent', 'asd_ais', 'guard_engine'];
207
- return aiActors.some((ai) => actor.toLowerCase().includes(ai));
107
+ /** 批量操作需要授权 */
108
+ _checkBatch(req, rule) {
109
+ if (!req.action?.includes('batch_')) return null;
110
+ if (req.data?.authorized || req.authorized) return null;
111
+ return { rule: rule.id, reason: '缺少授权标志', suggestion: '添加 authorized: true' };
208
112
  }
209
113
 
210
- /**
211
- * 从 action 字符串提取动词
212
- * 支持多种格式:
213
- * 'create' → 'create'
214
- * 'candidate:create' → 'create' (Gateway)
215
- * 'create_candidate' → 'create' (Legacy)
216
- */
217
- _extractVerb(action) {
114
+ // ─── 辅助方法 ──────────────────────────────────────────
115
+
116
+ _verb(action) {
218
117
  if (!action) return '';
219
- // Gateway resource:verb 格式
220
- if (action.includes(':')) return action.split(':').pop();
221
- // 其他格式直接返回
222
- return action;
118
+ return action.includes(':') ? action.split(':').pop() : action;
223
119
  }
224
120
 
225
- /**
226
- * action resource 提取资源名(单数形式)
227
- * 支持:
228
- * 'candidate:create' 'candidate' (Gateway)
229
- * '/candidates/123' → 'candidates' (REST path)
230
- * 'candidates' → 'candidates' (plain)
231
- */
232
- _extractResourceName(action, resource) {
233
- // 优先从 Gateway action 格式提取
234
- if (action?.includes(':')) {
235
- return action.split(':')[0];
236
- }
237
- // 从 resource 提取
238
- if (typeof resource === 'string') {
239
- if (resource.startsWith('/')) {
240
- const match = resource.match(/^\/([^/]+)/);
241
- return match ? match[1] : resource;
242
- }
243
- return resource;
121
+ _resource(action, resource) {
122
+ if (action?.includes(':')) return action.split(':')[0];
123
+ if (typeof resource === 'string' && resource.startsWith('/')) {
124
+ const m = resource.match(/^\/([^/]+)/);
125
+ return m ? m[1] : resource;
244
126
  }
245
- return '';
127
+ return resource || '';
246
128
  }
247
129
 
248
- /**
249
- * 强制验证(违规时抛出异常)
250
- */
251
- async enforce(request) {
252
- const result = await this.validate(request);
253
- if (!result.compliant) {
254
- throw new ConstitutionViolation(result.violations);
130
+ _isAI(actor) {
131
+ return ['external_agent', 'chat_agent'].some(a => actor?.toLowerCase().includes(a));
132
+ }
133
+
134
+ // ─── 旧格式兼容 ───────────────────────────────────────
135
+
136
+ /** 兼容旧 priorities 格式 */
137
+ async _checkLegacyPriority(priority, request) {
138
+ const violations = [];
139
+ if (priority.id === 1) {
140
+ const v = this._checkDestructive(request, { id: 'destructive_confirm' });
141
+ if (v) violations.push(v);
142
+ const v2 = this._checkContent(request, { id: 'content_required' });
143
+ if (v2) violations.push(v2);
255
144
  }
256
- return result;
145
+ if (priority.id === 2) {
146
+ const v = this._checkAiRecipe(request, { id: 'ai_no_direct_recipe' });
147
+ if (v) violations.push(v);
148
+ const v2 = this._checkBatch(request, { id: 'batch_authorized' });
149
+ if (v2) violations.push(v2);
150
+ }
151
+ return violations;
257
152
  }
258
153
  }
259
154