autosnippet 2.12.0 → 2.13.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.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * GuardFeedbackLoop — Guard ↔ Recipe 闭环联动
3
+ *
4
+ * 功能:
5
+ * 1. 对比当前和历史 violations,检测已修复的违规
6
+ * 2. 已修复违规如有 fixSuggestion → 自动 confirmUsage(记录 Recipe 使用)
7
+ * 3. 集成到 guardAuditFiles MCP handler 和 GuardHandler (FileWatcher)
8
+ */
9
+
10
+ import Logger from '../../infrastructure/logging/Logger.js';
11
+
12
+ export class GuardFeedbackLoop {
13
+ /**
14
+ * @param {import('./ViolationsStore.js').ViolationsStore} violationsStore
15
+ * @param {import('../quality/FeedbackCollector.js').FeedbackCollector} feedbackCollector
16
+ * @param {object} [options]
17
+ * @param {import('./GuardCheckEngine.js').GuardCheckEngine} [options.guardCheckEngine] - 用于查找规则
18
+ */
19
+ constructor(violationsStore, feedbackCollector, options = {}) {
20
+ this.violationsStore = violationsStore;
21
+ this.feedbackCollector = feedbackCollector;
22
+ this.guardCheckEngine = options.guardCheckEngine || null;
23
+ this.logger = Logger.getInstance();
24
+ }
25
+
26
+ /**
27
+ * 对比当前和历史 violations,检测已修复的违规
28
+ * @param {{ violations: Array<{ruleId: string}> }} currentResult - 本次检查结果
29
+ * @param {string} filePath - 文件路径
30
+ * @returns {Array<{ ruleId: string, filePath: string, fixRecipeId: string }>} 已修复且有 Recipe 关联的列表
31
+ */
32
+ detectFixedViolations(currentResult, filePath) {
33
+ if (!this.violationsStore) return [];
34
+
35
+ try {
36
+ const previousRuns = this.violationsStore.getRunsByFile(filePath);
37
+ if (previousRuns.length === 0) return [];
38
+
39
+ // 取最近一次运行结果
40
+ const lastRun = previousRuns[previousRuns.length - 1];
41
+ const lastRuleIds = new Set((lastRun.violations || []).map(v => v.ruleId));
42
+ const currentRuleIds = new Set((currentResult.violations || []).map(v => v.ruleId));
43
+
44
+ const fixed = [];
45
+ for (const ruleId of lastRuleIds) {
46
+ if (!currentRuleIds.has(ruleId)) {
47
+ // 该规则的违规已消失 → 修复了
48
+ const fixRecipeId = this._findFixRecipe(ruleId, lastRun.violations);
49
+ if (fixRecipeId) {
50
+ fixed.push({ ruleId, filePath, fixRecipeId });
51
+ }
52
+ }
53
+ }
54
+
55
+ return fixed;
56
+ } catch (err) {
57
+ this.logger.debug(`[GuardFeedbackLoop] detectFixedViolations error: ${err.message}`);
58
+ return [];
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 对已修复的违规自动确认使用
64
+ * @param {Array<{ ruleId: string, filePath: string, fixRecipeId: string }>} fixedList
65
+ */
66
+ autoConfirmUsage(fixedList) {
67
+ if (!this.feedbackCollector || !fixedList?.length) return;
68
+
69
+ for (const { ruleId, fixRecipeId, filePath } of fixedList) {
70
+ try {
71
+ this.feedbackCollector.record('insert', fixRecipeId, {
72
+ source: 'guard_fix_detection',
73
+ automatic: true,
74
+ ruleId,
75
+ filePath,
76
+ });
77
+ this.logger.info(`[GuardFeedbackLoop] Auto-confirmed usage: recipe=${fixRecipeId} from fixing rule=${ruleId}`);
78
+ } catch (err) {
79
+ this.logger.debug(`[GuardFeedbackLoop] autoConfirmUsage error: ${err.message}`);
80
+ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 一站式处理:检测修复 + 自动确认
86
+ * 供 MCP handler 和 GuardHandler 集成调用
87
+ * @param {{ violations: Array }} currentResult
88
+ * @param {string} filePath
89
+ */
90
+ processFixDetection(currentResult, filePath) {
91
+ const fixed = this.detectFixedViolations(currentResult, filePath);
92
+ if (fixed.length > 0) {
93
+ this.autoConfirmUsage(fixed);
94
+ this.logger.info(`[GuardFeedbackLoop] Detected ${fixed.length} fixed violations in ${filePath}`);
95
+ }
96
+ return fixed;
97
+ }
98
+
99
+ /**
100
+ * 从 violation 或 GuardCheckEngine 查找 fixRecipeId
101
+ */
102
+ _findFixRecipe(ruleId, violations) {
103
+ // 先从 violation 本身的 fixSuggestion 查找
104
+ for (const v of (violations || [])) {
105
+ if (v.ruleId === ruleId && v.fixSuggestion) {
106
+ return v.fixSuggestion.replace(/^recipe:/, '');
107
+ }
108
+ }
109
+
110
+ // 再从 GuardCheckEngine 的规则定义中查找
111
+ if (this.guardCheckEngine) {
112
+ try {
113
+ const rules = this.guardCheckEngine.getRules();
114
+ const rule = rules.find(r => r.id === ruleId);
115
+ if (rule?.fixSuggestion) {
116
+ return rule.fixSuggestion.replace(/^recipe:/, '');
117
+ }
118
+ } catch { /* ignore */ }
119
+ }
120
+
121
+ return null;
122
+ }
123
+ }
124
+
125
+ export default GuardFeedbackLoop;
@@ -44,9 +44,12 @@ export class GuardService {
44
44
  preconditions: [],
45
45
  sideEffects: [],
46
46
  guards: [{
47
- pattern: data.pattern,
47
+ ...(data.pattern ? { pattern: data.pattern } : {}),
48
48
  severity: data.severity || 'warning',
49
49
  message: data.description || '',
50
+ type: data.type || 'regex',
51
+ ...(data.astQuery ? { astQuery: data.astQuery } : {}),
52
+ ...(data.fixSuggestion ? { fixSuggestion: data.fixSuggestion } : {}),
50
53
  }],
51
54
  },
52
55
  tags: data.languages || [],
@@ -241,6 +244,7 @@ export class GuardService {
241
244
 
242
245
  /**
243
246
  * 验证创建输入
247
+ * type='regex' 时 pattern 必须提供;type='ast' 时 astQuery 必须提供
244
248
  */
245
249
  _validateCreateInput(data) {
246
250
  if (!data.name || data.name.trim().length === 0) {
@@ -249,8 +253,16 @@ export class GuardService {
249
253
  if (!data.description || data.description.trim().length === 0) {
250
254
  throw new ValidationError('Rule description is required');
251
255
  }
252
- if (!data.pattern || data.pattern.trim().length === 0) {
253
- throw new ValidationError('Pattern is required');
256
+
257
+ const ruleType = data.type || 'regex';
258
+ if (ruleType === 'ast') {
259
+ if (!data.astQuery || !data.astQuery.queryType) {
260
+ throw new ValidationError('AST query with queryType is required for type=ast rules');
261
+ }
262
+ } else {
263
+ if (!data.pattern || data.pattern.trim().length === 0) {
264
+ throw new ValidationError('Pattern is required for regex rules');
265
+ }
254
266
  }
255
267
  }
256
268
  }
@@ -31,7 +31,9 @@ export class RuleLearner {
31
31
  recordTrigger(ruleId, context = {}) {
32
32
  const stat = this.#ensureStat(ruleId);
33
33
  stat.triggers++;
34
- stat.lastTriggered = new Date().toISOString();
34
+ const now = new Date().toISOString();
35
+ stat.lastTriggered = now;
36
+ if (!stat.firstTriggered) stat.firstTriggered = now;
35
37
  this.#save();
36
38
  }
37
39
 
@@ -123,6 +125,118 @@ export class RuleLearner {
123
125
  this.#save();
124
126
  }
125
127
 
128
+ /**
129
+ * 基于历史数据提出规则优化建议
130
+ * 策略 1: 高误报规则 → 建议调整
131
+ * 策略 2: 高触发且高精度 → 建议创建项目特化版本
132
+ * @returns {Array<{ type: string, ruleId: string, message: string, confidence: number, evidence: object }>}
133
+ */
134
+ suggestRules() {
135
+ const suggestions = [];
136
+
137
+ // 策略 1: 从高误报规则推导改进建议
138
+ const problematic = this.getProblematicRules();
139
+ for (const p of problematic) {
140
+ if (p.recommendation === 'tune') {
141
+ suggestions.push({
142
+ type: 'tune_existing',
143
+ ruleId: p.ruleId,
144
+ message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议调整正则或收窄语言范围`,
145
+ confidence: 0.7,
146
+ evidence: p.metrics,
147
+ });
148
+ } else if (p.recommendation === 'disable') {
149
+ suggestions.push({
150
+ type: 'disable',
151
+ ruleId: p.ruleId,
152
+ message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议禁用`,
153
+ confidence: 0.8,
154
+ evidence: p.metrics,
155
+ });
156
+ }
157
+ }
158
+
159
+ // 策略 2: 高触发 + 高精度内置规则 → 建议创建项目定制版
160
+ const allStats = this.getAllStats();
161
+ for (const [ruleId, stat] of Object.entries(allStats)) {
162
+ if (stat.triggers > 50 && (stat.metrics?.precision ?? 1) > 0.8) {
163
+ suggestions.push({
164
+ type: 'specialize',
165
+ ruleId,
166
+ message: `规则 ${ruleId} 触发 ${stat.triggers} 次且精准度高 (${((stat.metrics?.precision ?? 1) * 100).toFixed(0)}%),建议创建项目定制版本`,
167
+ confidence: 0.6,
168
+ evidence: stat.metrics,
169
+ });
170
+ }
171
+ }
172
+
173
+ // 策略 3: 长期无触发的规则 → 可能不适用
174
+ for (const [ruleId, stat] of Object.entries(allStats)) {
175
+ if (stat.triggers === 0 && stat.lastTriggered) {
176
+ const daysSinceLastTrigger = (Date.now() - new Date(stat.lastTriggered).getTime()) / 86400000;
177
+ if (daysSinceLastTrigger > 30) {
178
+ suggestions.push({
179
+ type: 'review_unused',
180
+ ruleId,
181
+ message: `规则 ${ruleId} 超过 30 天未触发,建议审查是否仍需保留`,
182
+ confidence: 0.4,
183
+ evidence: { daysSinceLastTrigger: Math.round(daysSinceLastTrigger), triggers: stat.triggers },
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ return suggestions.sort((a, b) => b.confidence - a.confidence);
190
+ }
191
+
192
+ /**
193
+ * 追踪规则创建后的效果
194
+ * 对比首次触发后的表现,判断规则是否有效
195
+ * @param {string} ruleId
196
+ * @returns {{ status: string, triggers: number, precision: number, recommendation: string, daysSinceFirstTrigger?: number }}
197
+ */
198
+ trackRuleEffectiveness(ruleId) {
199
+ const stat = this.#data.ruleStats[ruleId];
200
+ if (!stat) return { status: 'no_data', triggers: 0, precision: 1, recommendation: 'monitor' };
201
+
202
+ const firstTriggered = stat.firstTriggered || stat.lastTriggered;
203
+ if (!firstTriggered) return { status: 'no_triggers', triggers: 0, precision: 1, recommendation: 'monitor' };
204
+
205
+ const daysSinceFirstTrigger = (Date.now() - new Date(firstTriggered).getTime()) / 86400000;
206
+
207
+ // 不足 14 天 → 观察期
208
+ if (daysSinceFirstTrigger < 14) {
209
+ return {
210
+ status: 'monitoring',
211
+ triggers: stat.triggers,
212
+ precision: this.getMetrics(ruleId).precision,
213
+ recommendation: 'wait',
214
+ daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger),
215
+ };
216
+ }
217
+
218
+ const metrics = this.getMetrics(ruleId);
219
+
220
+ // 14 天后判定
221
+ if (metrics.precision < 0.5 && stat.triggers >= PROBLEMATIC_THRESHOLD.minTriggers) {
222
+ return {
223
+ status: 'ineffective',
224
+ triggers: stat.triggers,
225
+ precision: metrics.precision,
226
+ recommendation: 'review_or_disable',
227
+ daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger),
228
+ };
229
+ }
230
+
231
+ return {
232
+ status: 'effective',
233
+ triggers: stat.triggers,
234
+ precision: metrics.precision,
235
+ recommendation: 'keep',
236
+ daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger),
237
+ };
238
+ }
239
+
126
240
  // ─── 私有 ─────────────────────────────────────────────
127
241
 
128
242
  #ensureStat(ruleId) {
@@ -132,6 +246,7 @@ export class RuleLearner {
132
246
  correct: 0,
133
247
  falsePositive: 0,
134
248
  falseNegative: 0,
249
+ firstTriggered: null,
135
250
  lastTriggered: null,
136
251
  lastFeedback: null,
137
252
  };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * SourceFileCollector — 递归收集项目源文件
3
+ *
4
+ * 从 GuardHandler 提取的公共工具,供 ComplianceReporter / guard:ci / guard:staged 复用
5
+ */
6
+
7
+ import { join, extname } from 'node:path';
8
+ import { readdir, readFile } from 'node:fs/promises';
9
+
10
+ /** 支持审计的源文件扩展名 */
11
+ export const SOURCE_EXTS = new Set([
12
+ '.m', '.mm', '.h', '.swift',
13
+ '.c', '.cpp', '.cc', '.cxx', '.hpp',
14
+ '.js', '.ts', '.jsx', '.tsx',
15
+ '.java', '.kt', '.py', '.rb', '.go', '.rs',
16
+ ]);
17
+
18
+ /** 跳过的目录 */
19
+ const SKIP_DIRS = new Set([
20
+ 'node_modules', '.git', 'build', 'DerivedData',
21
+ 'Pods', '.build', 'vendor', 'dist', '.next',
22
+ 'Carthage', 'xcuserdata', '__pycache__',
23
+ ]);
24
+
25
+ /**
26
+ * 递归收集目录下所有源文件路径
27
+ * @param {string} dir - 根目录
28
+ * @param {object} options
29
+ * @param {Set<string>} [options.extensions] - 允许的扩展名(默认 SOURCE_EXTS)
30
+ * @param {Set<string>} [options.skipDirs] - 跳过的目录名(默认 SKIP_DIRS)
31
+ * @param {number} [options.maxFiles] - 最大文件数量(默认无限制)
32
+ * @returns {Promise<string[]>} 文件路径列表
33
+ */
34
+ export async function collectSourceFiles(dir, options = {}) {
35
+ const {
36
+ extensions = SOURCE_EXTS,
37
+ skipDirs = SKIP_DIRS,
38
+ maxFiles = Infinity,
39
+ } = options;
40
+
41
+ const files = [];
42
+
43
+ async function walk(currentDir) {
44
+ if (files.length >= maxFiles) return;
45
+
46
+ let entries;
47
+ try {
48
+ entries = await readdir(currentDir, { withFileTypes: true });
49
+ } catch {
50
+ return; // 权限不足等情况跳过
51
+ }
52
+
53
+ for (const entry of entries) {
54
+ if (files.length >= maxFiles) return;
55
+ if (entry.name.startsWith('.') && entry.name !== '.') continue;
56
+
57
+ const fullPath = join(currentDir, entry.name);
58
+ if (entry.isDirectory()) {
59
+ if (!skipDirs.has(entry.name)) {
60
+ await walk(fullPath);
61
+ }
62
+ } else if (entry.isFile() && extensions.has(extname(entry.name).toLowerCase())) {
63
+ files.push(fullPath);
64
+ }
65
+ }
66
+ }
67
+
68
+ await walk(dir);
69
+ return files;
70
+ }
71
+
72
+ /**
73
+ * 收集源文件并读取内容
74
+ * @param {string} dir - 根目录
75
+ * @param {object} options - collectSourceFiles 选项
76
+ * @returns {Promise<Array<{path: string, content: string}>>}
77
+ */
78
+ export async function collectSourceFilesWithContent(dir, options = {}) {
79
+ const paths = await collectSourceFiles(dir, options);
80
+ const results = [];
81
+
82
+ for (const filePath of paths) {
83
+ try {
84
+ const content = await readFile(filePath, 'utf-8');
85
+ results.push({ path: filePath, content });
86
+ } catch {
87
+ // 读取失败跳过
88
+ }
89
+ }
90
+
91
+ return results;
92
+ }
93
+
94
+ export default { collectSourceFiles, collectSourceFilesWithContent, SOURCE_EXTS };
@@ -77,7 +77,7 @@ export class ViolationsStore {
77
77
  */
78
78
  getRecentRuns(n = 20) {
79
79
  const rows = this.#db.prepare(
80
- 'SELECT * FROM guard_violations ORDER BY created_at DESC LIMIT ?'
80
+ 'SELECT * FROM guard_violations ORDER BY created_at DESC, rowid DESC LIMIT ?'
81
81
  ).all(n);
82
82
  return rows.reverse().map(r => this.#rowToRun(r));
83
83
  }
@@ -104,6 +104,64 @@ export class ViolationsStore {
104
104
  };
105
105
  }
106
106
 
107
+ /**
108
+ * 按规则 ID 聚合统计
109
+ * 利用 SQLite json_each 展开 violations_json 数组
110
+ * @returns {Array<{ruleId: string, severity: string, count: number}>}
111
+ */
112
+ getStatsByRule() {
113
+ try {
114
+ return this.#db.prepare(`
115
+ SELECT
116
+ json_extract(j.value, '$.ruleId') AS ruleId,
117
+ json_extract(j.value, '$.severity') AS severity,
118
+ COUNT(*) AS count
119
+ FROM guard_violations gv, json_each(gv.violations_json) j
120
+ WHERE json_extract(j.value, '$.ruleId') IS NOT NULL
121
+ GROUP BY ruleId, severity
122
+ ORDER BY count DESC
123
+ `).all();
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ /**
130
+ * 获取趋势数据 — 对比最近两次运行
131
+ * @returns {{ errorsChange: number, warningsChange: number, latestErrors: number, latestWarnings: number, previousErrors: number, previousWarnings: number }}
132
+ */
133
+ getTrend() {
134
+ const recent = this.getRecentRuns(2);
135
+ if (recent.length < 2) {
136
+ const latest = recent[0]?.violations || [];
137
+ return {
138
+ errorsChange: 0,
139
+ warningsChange: 0,
140
+ latestErrors: latest.filter(v => v.severity === 'error').length,
141
+ latestWarnings: latest.filter(v => v.severity === 'warning').length,
142
+ previousErrors: 0,
143
+ previousWarnings: 0,
144
+ hasHistory: false,
145
+ };
146
+ }
147
+
148
+ const [prev, latest] = recent;
149
+ const latestErrors = latest.violations.filter(v => v.severity === 'error').length;
150
+ const latestWarnings = latest.violations.filter(v => v.severity === 'warning').length;
151
+ const previousErrors = prev.violations.filter(v => v.severity === 'error').length;
152
+ const previousWarnings = prev.violations.filter(v => v.severity === 'warning').length;
153
+
154
+ return {
155
+ errorsChange: latestErrors - previousErrors,
156
+ warningsChange: latestWarnings - previousWarnings,
157
+ latestErrors,
158
+ latestWarnings,
159
+ previousErrors,
160
+ previousWarnings,
161
+ hasHistory: true,
162
+ };
163
+ }
164
+
107
165
  // ─── 清除 ─────────────────────────────────────────────
108
166
 
109
167
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "AutoSnippet - 连接开发者、AI 与项目知识库的工具",
5
5
  "type": "module",
6
6
  "main": "lib/bootstrap.js",
@@ -0,0 +1,80 @@
1
+ # AutoSnippet Guard CI/CD Check
2
+ # 在 push 和 PR 时自动运行 Guard 合规检查
3
+ #
4
+ # 使用方法:将此文件复制到项目的 .github/workflows/ 目录
5
+
6
+ name: AutoSnippet Guard Check
7
+
8
+ on:
9
+ push:
10
+ branches: [main, develop]
11
+ pull_request:
12
+ branches: [main, develop]
13
+
14
+ jobs:
15
+ guard:
16
+ runs-on: ubuntu-latest
17
+ name: Guard Compliance Check
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Setup Node.js
24
+ uses: actions/setup-node@v4
25
+ with:
26
+ node-version: '20'
27
+
28
+ - name: Install AutoSnippet
29
+ run: npm install -g autosnippet
30
+
31
+ - name: Initialize workspace
32
+ run: asd setup --dir .
33
+
34
+ - name: Run Guard CI check
35
+ run: |
36
+ asd guard:ci . \
37
+ --report json \
38
+ --output guard-report.json \
39
+ --max-warnings 20 \
40
+ --min-score 70
41
+
42
+ - name: Upload Guard Report
43
+ uses: actions/upload-artifact@v4
44
+ if: always()
45
+ with:
46
+ name: guard-report
47
+ path: guard-report.json
48
+ retention-days: 30
49
+
50
+ - name: Comment PR with results
51
+ if: github.event_name == 'pull_request' && always()
52
+ uses: actions/github-script@v7
53
+ with:
54
+ script: |
55
+ const fs = require('fs');
56
+ try {
57
+ const report = JSON.parse(fs.readFileSync('guard-report.json', 'utf8'));
58
+ const { qualityGate, summary } = report;
59
+ const icon = qualityGate.status === 'PASS' ? '✅' : qualityGate.status === 'WARN' ? '⚠️' : '❌';
60
+
61
+ const body = [
62
+ `## ${icon} Guard Compliance Report`,
63
+ '',
64
+ `| Metric | Value |`,
65
+ `|--------|-------|`,
66
+ `| Quality Gate | ${qualityGate.status} (Score: ${qualityGate.score}/100) |`,
67
+ `| Files Scanned | ${summary.filesScanned} |`,
68
+ `| Errors | ${summary.errors} |`,
69
+ `| Warnings | ${summary.warnings} |`,
70
+ ].join('\n');
71
+
72
+ await github.rest.issues.createComment({
73
+ issue_number: context.issue.number,
74
+ owner: context.repo.owner,
75
+ repo: context.repo.repo,
76
+ body,
77
+ });
78
+ } catch (err) {
79
+ console.log('Could not post PR comment:', err.message);
80
+ }
@@ -0,0 +1,33 @@
1
+ #!/bin/sh
2
+ # AutoSnippet Guard pre-commit hook
3
+ #
4
+ # 安装方法:
5
+ # cp templates/pre-commit-guard.sh .git/hooks/pre-commit
6
+ # chmod +x .git/hooks/pre-commit
7
+ #
8
+ # 或使用 husky:
9
+ # npx husky add .husky/pre-commit "sh templates/pre-commit-guard.sh"
10
+
11
+ # 检查 asd 是否可用
12
+ if ! command -v asd &> /dev/null; then
13
+ echo "⚠️ AutoSnippet (asd) not found. Skipping Guard check."
14
+ echo " Install: npm install -g autosnippet"
15
+ exit 0
16
+ fi
17
+
18
+ echo "🛡️ Running Guard check on staged files..."
19
+
20
+ # 运行 guard:staged 检查
21
+ asd guard:staged --fail-on-error
22
+
23
+ EXIT_CODE=$?
24
+
25
+ if [ $EXIT_CODE -ne 0 ]; then
26
+ echo ""
27
+ echo "❌ Guard check failed. Fix violations before committing."
28
+ echo " Use 'git commit --no-verify' to bypass (not recommended)."
29
+ exit $EXIT_CODE
30
+ fi
31
+
32
+ echo "✅ Guard check passed."
33
+ exit 0