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.
- package/README.md +111 -37
- package/bin/cli.js +144 -0
- package/config/default.json +5 -0
- package/lib/core/AstAnalyzer.js +179 -0
- package/lib/external/mcp/handlers/guard.js +7 -1
- package/lib/http/routes/guardRules.js +27 -0
- package/lib/injection/ServiceContainer.js +29 -0
- package/lib/service/automation/handlers/GuardHandler.js +11 -0
- package/lib/service/guard/ComplianceReporter.js +335 -0
- package/lib/service/guard/GuardCheckEngine.js +171 -16
- package/lib/service/guard/GuardFeedbackLoop.js +125 -0
- package/lib/service/guard/GuardService.js +15 -3
- package/lib/service/guard/RuleLearner.js +116 -1
- package/lib/service/guard/SourceFileCollector.js +94 -0
- package/lib/service/guard/ViolationsStore.js +59 -1
- package/package.json +1 -1
- package/templates/guard-ci.yml +80 -0
- package/templates/pre-commit-guard.sh +33 -0
package/README.md
CHANGED
|
@@ -31,13 +31,15 @@ AI 编码助手生成的代码往往脱离项目上下文——不知道团队
|
|
|
31
31
|
|
|
32
32
|
| 概念 | 说明 |
|
|
33
33
|
|------|------|
|
|
34
|
-
| **Recipe** | 知识库的基本单元——一段代码模式 + 使用说明 +
|
|
34
|
+
| **Recipe** | 知识库的基本单元——一段代码模式 + 使用说明 + 元数据,以 Markdown 文件(`AutoSnippet/recipes/*.md`)为 Source of Truth,SQLite 作为检索缓存 |
|
|
35
35
|
| **Candidate** | 待审核的候选知识——来自 AI 扫描、手动提交、剪贴板或 Bootstrap 冷启动,经 Dashboard 人工审核后晋升为 Recipe |
|
|
36
|
-
| **Dashboard** | Web 管理后台(`asd ui`),10
|
|
36
|
+
| **Dashboard** | Web 管理后台(`asd ui`),10+ 功能视图:Recipes / Candidates / Knowledge / AI Chat / SPM Explorer / 知识图谱 / 依赖图 / Guard / Skills / Xcode 模拟器 / Help |
|
|
37
37
|
| **Guard** | 代码审查引擎——基于知识库中的规则对代码做合规检查,支持文件 / Target / 项目三级范围 |
|
|
38
38
|
| **Skills** | 13 个 Cursor Agent 技能包——覆盖候选生成、冷启动、Guard 审计、意图路由、生命周期管理等场景 |
|
|
39
|
-
| **Bootstrap** | 冷启动引擎——自动扫描 SPM Target + AST 分析,9 维度启发式提取代码模式,AI 精炼后生成 Candidate |
|
|
40
|
-
| **
|
|
39
|
+
| **Bootstrap** | 冷启动引擎——自动扫描 SPM Target + AST 分析,9 维度启发式提取代码模式,AI 精炼后生成 Candidate;支持增量模式(IncrementalBootstrap),文件变更检测 + 受影响维度重跑 |
|
|
40
|
+
| **Agent Memory** | 四层记忆架构——WorkingMemory(会话级)→ EpisodicMemory(跨维度共享)→ ProjectSemanticMemory(项目级永久语义记忆)→ ToolResultCache(工具结果去重),支撑 Bootstrap 和 ChatAgent 的跨对话知识积累 |
|
|
41
|
+
| **ChatAgent** | 多 Agent 协作对话系统(Analyst + Producer),支持项目感知、信心信号、组合工具链和跨对话记忆 |
|
|
42
|
+
| **CodeEntityGraph** | 代码实体关系图谱——基于 AST 解析构建 class / protocol / category / module 间的继承、遵循、依赖、数据流等关系,供 Bootstrap 和搜索使用 |
|
|
41
43
|
|
|
42
44
|
## 快速开始
|
|
43
45
|
|
|
@@ -63,36 +65,102 @@ asd status # 自检项目根、AI Provider、索引、Dashboard
|
|
|
63
65
|
|
|
64
66
|
## 工作流
|
|
65
67
|
|
|
66
|
-
###
|
|
68
|
+
### 端到端使用流程
|
|
69
|
+
|
|
70
|
+
从零开始到知识库持续运转的完整路径——以 Cursor 为例:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
74
|
+
│ ① 初始化 │──→│ ② 冷启动 │──→│ ③ 逐Target │──→│ ④ 审核发布 │──→│ ⑤ 注入 IDE │
|
|
75
|
+
│ asd setup │ │ Bootstrap │ │ 扫描 │ │ Dashboard │ │ Cursor │
|
|
76
|
+
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
|
|
77
|
+
│
|
|
78
|
+
┌───────────────────────────────────────────────────────────────────────┘
|
|
79
|
+
↓
|
|
80
|
+
┌─────────────┐ ┌─────────────┐
|
|
81
|
+
│ ⑥ AI 按 │──→│ ⑦ 新模式 │──→ 回到 ③
|
|
82
|
+
│ 规范生成 │ │ 再沉淀 │
|
|
83
|
+
└─────────────┘ └─────────────┘
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**① 初始化项目**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cd /path/to/your-project
|
|
90
|
+
asd setup # 创建 AutoSnippet/ 目录、数据库、配置文件
|
|
91
|
+
asd install:full # 安装 Cursor Skills (13个) + MCP 配置 + Cursor Rules
|
|
92
|
+
asd ui # 启动 Dashboard + API 服务 + 文件监听
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
完成后,你的项目目录下会生成 `.cursor/mcp.json`、`.cursor/skills/`、`.cursor/rules/` 等集成文件,Cursor 已经可以通过 MCP 与 AutoSnippet 通信。
|
|
96
|
+
|
|
97
|
+
**② 冷启动——全局扫描建立基线**
|
|
98
|
+
|
|
99
|
+
在 Cursor 中用自然语言触发冷启动:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
你:「对项目做一次全量冷启动,提取所有代码模式」
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Bootstrap 引擎自动完成:SPM Target 发现 → 文件收集 → AST 结构分析 → 9 维度启发式扫描(架构 / 命名 / 网络 / 数据流 / 错误处理等) → Analyst Agent 深度分析 → Producer Agent 格式化提交。
|
|
106
|
+
|
|
107
|
+
冷启动结束后,Dashboard Candidates 页面会出现数十条候选知识条目。
|
|
108
|
+
|
|
109
|
+
**③ 逐 Target 精细扫描**
|
|
110
|
+
|
|
111
|
+
冷启动覆盖全局,但每个 Target 的独特模式需要针对性提取:
|
|
67
112
|
|
|
68
113
|
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
│ ① 扫描提取 ② 人工审核 │
|
|
72
|
-
│ Cursor AI 扫描 Target ──→ Candidates ──→ Recipe 入库 │
|
|
73
|
-
│ asd ais <Target> Dashboard │
|
|
74
|
-
│ Bootstrap 冷启动 / 剪贴板 │
|
|
75
|
-
│ │
|
|
76
|
-
│ ③ AI 按规范生成 ④ 持续沉淀 │
|
|
77
|
-
│ Cursor/Copilot 检索 Recipe ──→ 生成代码 ──→ 好代码再入库 │
|
|
78
|
-
│ MCP 工具 / Xcode Snippet │
|
|
79
|
-
│ │
|
|
80
|
-
└─────────────────────────────────────────────────────────────┘
|
|
114
|
+
你:「扫描 NetworkModule 这个 Target,把里面的请求封装模式提取出来」
|
|
115
|
+
你:「分析 UIComponents Target 的自定义控件实现」
|
|
81
116
|
```
|
|
82
117
|
|
|
83
|
-
|
|
118
|
+
Cursor 调用 `get_targets` → `get_target_files` → 逐文件 AI 分析 → `submit_knowledge_batch`,将发现的代码模式作为 Candidate 提交到知识库。你可以逐个 Target 推进,每个 Target 的扫描结果会独立进入审核队列。
|
|
119
|
+
|
|
120
|
+
**④ 审核发布——人工把关质量**
|
|
121
|
+
|
|
122
|
+
打开 Dashboard(`asd ui`),进入 **Candidates** 页面:
|
|
123
|
+
|
|
124
|
+
- 候选按置信度排序,高信心的排在前面
|
|
125
|
+
- 点击候选卡片展开详情:查看代码片段、AI 分析理由、来源文件
|
|
126
|
+
- **接受** → 候选晋升为 Recipe,进入活跃知识库
|
|
127
|
+
- **编辑后接受** → 审核时可修改标题、描述、代码、标签
|
|
128
|
+
- **拒绝** → 低质量或重复的候选直接归档
|
|
129
|
+
|
|
130
|
+
批量操作:一键接受所有高信心候选,或一键拒绝低质量条目。
|
|
131
|
+
|
|
132
|
+
**⑤ 注入 Cursor——知识即规则**
|
|
84
133
|
|
|
85
|
-
|
|
134
|
+
Recipe 发布后,Cursor 立即可以通过三种通道获取知识:
|
|
135
|
+
|
|
136
|
+
| 通道 | 机制 | 时效 |
|
|
137
|
+
|------|------|------|
|
|
138
|
+
| **MCP 工具检索** | Cursor 通过 `autosnippet_search` 等 38 个工具实时查询知识库 | 实时 |
|
|
139
|
+
| **Cursor Rules** | `asd upgrade` 将 Recipe 导出为 `.cursor/rules/autosnippet-*.mdc` 文件 | 手动触发 |
|
|
140
|
+
| **Agent Skills** | 13 个 Skill 文档引导 Cursor 在正确场景自动调用知识库 | 常驻 |
|
|
141
|
+
|
|
142
|
+
**⑥ AI 按规范生成代码**
|
|
143
|
+
|
|
144
|
+
当你在 Cursor 中编写代码时,AI 会自动检索知识库:
|
|
86
145
|
|
|
87
146
|
```
|
|
88
|
-
|
|
89
|
-
Cursor →
|
|
90
|
-
→
|
|
147
|
+
你:「写一个网络请求方法,获取用户信息」
|
|
148
|
+
Cursor → 检索知识库 → 命中 Recipe: "Network Layer Pattern"
|
|
149
|
+
→ 按团队封装的 NetworkManager 生成代码,而非裸调 URLSession
|
|
91
150
|
```
|
|
92
151
|
|
|
93
|
-
|
|
152
|
+
Guard 规则也在同步工作——如果生成的代码违反了知识库中的规则(如 `kind=rule` 的条目),会实时提醒和纠正。
|
|
153
|
+
|
|
154
|
+
**⑦ 持续沉淀——知识库越用越好**
|
|
94
155
|
|
|
95
|
-
|
|
156
|
+
日常开发中发现新的代码模式或团队约定?随时沉淀:
|
|
157
|
+
|
|
158
|
+
- 在 Cursor 中:`「把这段 error handling 模式提取为知识库条目」`
|
|
159
|
+
- 在 Xcode 中:代码注释写 `// as:create` 然后 `⌘S`
|
|
160
|
+
- 在 Dashboard 中:AI Chat 对话式提交
|
|
161
|
+
- 通过剪贴板:复制代码后自动检测并建议入库
|
|
162
|
+
|
|
163
|
+
知识库形成飞轮:**代码沉淀 → Recipe 增长 → AI 生成质量提升 → 团队效率提高 → 更多代码模式沉淀**。
|
|
96
164
|
|
|
97
165
|
## Dashboard
|
|
98
166
|
|
|
@@ -100,13 +168,14 @@ Cursor → get_targets → get_target_files → 逐文件提取 → submit_knowl
|
|
|
100
168
|
|
|
101
169
|

|
|
102
170
|
|
|
103
|
-
|
|
171
|
+
**功能视图**:
|
|
104
172
|
|
|
105
173
|
| 视图 | 说明 |
|
|
106
174
|
|------|------|
|
|
107
175
|
| **Recipes** | 浏览、编辑、发布、弃用知识条目;详情抽屉支持 Markdown 编辑与关联关系管理 |
|
|
108
176
|
| **Candidates** | 审核 AI / 手动提交的候选,一键入库或批量操作,支持 AI 润色 |
|
|
109
|
-
| **
|
|
177
|
+
| **Knowledge** | 统一知识条目浏览(V3 格式),双列卡片布局,代码预览 + 详情抽屉 |
|
|
178
|
+
| **AI Chat** | ChatAgent 智能对话(Analyst 分析 + Producer 生产),项目感知 + 四层记忆架构 |
|
|
110
179
|
| **SPM Explorer** | SPM Target 浏览与扫描,候选 vs Recipe 对比抽屉,头文件编辑 |
|
|
111
180
|
| **Dep Graph** | 依赖关系图可视化 |
|
|
112
181
|
| **Knowledge Graph** | Recipe 关联关系的知识图谱可视化(依赖 / 扩展 / 冲突等),AI 自动发现关系,按 category 分组 |
|
|
@@ -123,9 +192,9 @@ Cursor → get_targets → get_target_files → 逐文件提取 → submit_knowl
|
|
|
123
192
|
|
|
124
193
|
AutoSnippet 为 Cursor 提供完整的 MCP + Skills 集成:
|
|
125
194
|
|
|
126
|
-
- **38 个 MCP 工具**:搜索(4 种模式)、Guard 检查、候选提交 / 校验 / 查重、知识图谱查询、Bootstrap 冷启动、Skills
|
|
195
|
+
- **38 个 MCP 工具**:搜索(4 种模式)、Guard 检查、候选提交 / 校验 / 查重、知识图谱查询、Bootstrap 冷启动、Skills 管理、知识生命周期等
|
|
127
196
|
- **13 个 Agent Skills**:`autosnippet-candidates`、`autosnippet-guard`、`autosnippet-coldstart`、`autosnippet-intent` 等,引导 AI 正确使用工具
|
|
128
|
-
- **写操作 Gateway 保护**:
|
|
197
|
+
- **写操作 Gateway 保护**:11 个写操作经过权限 / 宪法 / 审计三重检查
|
|
129
198
|
|
|
130
199
|
```bash
|
|
131
200
|
asd install:cursor-skill --mcp # 安装 Skills + MCP 配置
|
|
@@ -178,13 +247,13 @@ asd install:vscode-copilot # 配置 MCP 和 Copilot 指令
|
|
|
178
247
|
| **系统** | `health`、`capabilities` |
|
|
179
248
|
| **搜索** | `search`(统合入口)、`context_search`(4 层漏斗)、`keyword_search`、`semantic_search` |
|
|
180
249
|
| **Recipe 浏览** | `list_recipes`、`get_recipe`、`list_rules`、`patterns`、`list_facts`、`recipe_insights`、`confirm_usage` |
|
|
181
|
-
| **候选管理** | `validate_candidate`、`check_duplicate`、`submit_knowledge`、`submit_knowledge_batch`、`
|
|
250
|
+
| **候选管理** | `validate_candidate`、`check_duplicate`、`submit_knowledge`、`submit_knowledge_batch`、`enrich_candidates` |
|
|
182
251
|
| **知识图谱** | `graph_query`、`graph_impact`、`graph_path`、`graph_stats` |
|
|
183
252
|
| **项目结构** | `get_targets`、`get_target_files`、`get_target_metadata` |
|
|
184
253
|
| **Guard** | `guard_check`、`guard_audit_files`、`scan_project` |
|
|
185
254
|
| **冷启动** | `bootstrap_knowledge`、`bootstrap_refine` |
|
|
186
255
|
| **Skills** | `list_skills`、`load_skill`、`create_skill`、`delete_skill`、`update_skill`、`suggest_skills` |
|
|
187
|
-
|
|
|
256
|
+
| **知识管理** | `knowledge_lifecycle`、`compliance_report` |
|
|
188
257
|
|
|
189
258
|
## 配置
|
|
190
259
|
|
|
@@ -237,18 +306,23 @@ your-project/
|
|
|
237
306
|
│ AutoSnippet Core │
|
|
238
307
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
|
|
239
308
|
│ │ Gateway │ │ ChatAgent│ │ Bootstrap│ │ Dashboard │ │
|
|
240
|
-
│ │ (权限/ │ │ (Dual │ │ (
|
|
241
|
-
│ │ 宪法/ │ │ Agent + │ │
|
|
242
|
-
│ │ 审计) │ │ Memory) │ │ AI)
|
|
309
|
+
│ │ (权限/ │ │ (Dual │ │ (Incremen│ │ (React 19 +│ │
|
|
310
|
+
│ │ 宪法/ │ │ Agent + │ │ tal + │ │ Vite 6 + │ │
|
|
311
|
+
│ │ 审计) │ │ Memory) │ │ AST+AI) │ │ Tailwind) │ │
|
|
243
312
|
│ └──────────┘ └──────────┘ └──────────┘ └─────────────┘ │
|
|
244
313
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
245
|
-
│ │
|
|
314
|
+
│ │ Agent Memory (4-Tier): │ │
|
|
315
|
+
│ │ WorkingMemory → EpisodicMemory → │ │
|
|
316
|
+
│ │ ProjectSemanticMemory → ToolResultCache │ │
|
|
317
|
+
│ └────────────────────────────────────────────────────┘ │
|
|
318
|
+
│ ┌────────────────────────────────────────────────────┐ │
|
|
319
|
+
│ │ 14 Services: Recipe │ Candidate │ Guard │ Search │ │
|
|
246
320
|
│ │ Knowledge Graph │ SPM │ Bootstrap │ Chat │ Skills │ │
|
|
247
|
-
│ │ Quality │ Context │ Automation │ Snippet
|
|
321
|
+
│ │ Quality │ Context │ Automation │ Snippet │ Cursor │ │
|
|
248
322
|
│ └────────────────────────────────────────────────────┘ │
|
|
249
323
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
250
324
|
│ │ Core: Gateway │ Constitution │ Permission │ AST │ │
|
|
251
|
-
│ │ Session │ Capability
|
|
325
|
+
│ │ Session │ Capability │ CodeEntityGraph │ │
|
|
252
326
|
│ └────────────────────────────────────────────────────┘ │
|
|
253
327
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
254
328
|
│ │ Storage: SQLite (better-sqlite3) + 向量索引 │ │
|
|
@@ -267,7 +341,7 @@ your-project/
|
|
|
267
341
|
| **前端** | React 19 + TypeScript 5 + Vite 6 + Tailwind CSS 4 |
|
|
268
342
|
| **AI** | Gemini / OpenAI / Claude(通过 AiProvider 抽象层) |
|
|
269
343
|
| **AST** | Tree-sitter(Swift / ObjC) |
|
|
270
|
-
| **搜索** |
|
|
344
|
+
| **搜索** | 5 层检索管线:InvertedIndex → Semantic Rerank → CoarseRanker (E-E-A-T) → MultiSignalRanker → RetrievalFunnel |
|
|
271
345
|
| **实时通信** | WebSocket(Socket.IO),Dashboard 实时更新 |
|
|
272
346
|
| **动画** | Framer Motion |
|
|
273
347
|
| **代码高亮** | Prism.js + react-syntax-highlighter |
|
package/bin/cli.js
CHANGED
|
@@ -221,6 +221,150 @@ program
|
|
|
221
221
|
}
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
// ─────────────────────────────────────────────────────
|
|
225
|
+
// guard:ci 命令
|
|
226
|
+
// ─────────────────────────────────────────────────────
|
|
227
|
+
program
|
|
228
|
+
.command('guard:ci [path]')
|
|
229
|
+
.description('CI/CD 模式运行全项目 Guard 检查')
|
|
230
|
+
.option('--fail-on-error', '有 error 级违规时 exit 1', true)
|
|
231
|
+
.option('--fail-on-warning', '超过 warning 阈值时 exit 2')
|
|
232
|
+
.option('--max-warnings <n>', 'warning 阈值', '20')
|
|
233
|
+
.option('--report <format>', '报告格式: json | text | markdown', 'text')
|
|
234
|
+
.option('--output <file>', '报告输出文件')
|
|
235
|
+
.option('--min-score <n>', 'Quality Gate 最低分', '70')
|
|
236
|
+
.option('--max-files <n>', '最大扫描文件数', '500')
|
|
237
|
+
.action(async (scanPath, opts) => {
|
|
238
|
+
try {
|
|
239
|
+
const projectRoot = resolve(scanPath || '.');
|
|
240
|
+
const { bootstrap, container } = await initContainer({ projectRoot });
|
|
241
|
+
const reporter = container.get('complianceReporter');
|
|
242
|
+
|
|
243
|
+
const report = await reporter.generate(projectRoot, {
|
|
244
|
+
qualityGate: {
|
|
245
|
+
maxErrors: 0,
|
|
246
|
+
maxWarnings: parseInt(opts.maxWarnings, 10),
|
|
247
|
+
minScore: parseInt(opts.minScore, 10),
|
|
248
|
+
},
|
|
249
|
+
maxFiles: parseInt(opts.maxFiles, 10),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// 输出报告
|
|
253
|
+
if (opts.report === 'json') {
|
|
254
|
+
const output = JSON.stringify(report, null, 2);
|
|
255
|
+
if (opts.output) {
|
|
256
|
+
const { writeFileSync } = await import('fs');
|
|
257
|
+
writeFileSync(opts.output, output, 'utf8');
|
|
258
|
+
console.log(`Report written to ${opts.output}`);
|
|
259
|
+
} else {
|
|
260
|
+
console.log(output);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
reporter.printReport(report, { format: opts.report });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 如果也要写文件(非 JSON 格式)
|
|
267
|
+
if (opts.output && opts.report !== 'json') {
|
|
268
|
+
const { writeFileSync } = await import('fs');
|
|
269
|
+
writeFileSync(opts.output, JSON.stringify(report, null, 2), 'utf8');
|
|
270
|
+
console.log(`Report data written to ${opts.output}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await bootstrap.shutdown();
|
|
274
|
+
|
|
275
|
+
// Exit code
|
|
276
|
+
if (report.qualityGate.status === 'FAIL') {
|
|
277
|
+
process.exit(report.summary.errors > 0 ? 1 : 2);
|
|
278
|
+
}
|
|
279
|
+
process.exit(0);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.error('Error:', err.message);
|
|
282
|
+
if (process.env.ASD_DEBUG === '1') console.error(err.stack);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ─────────────────────────────────────────────────────
|
|
288
|
+
// guard:staged 命令
|
|
289
|
+
// ─────────────────────────────────────────────────────
|
|
290
|
+
program
|
|
291
|
+
.command('guard:staged')
|
|
292
|
+
.description('检查 git staged 文件')
|
|
293
|
+
.option('--fail-on-error', '有 error 时 exit 1', true)
|
|
294
|
+
.option('--json', '以 JSON 格式输出')
|
|
295
|
+
.action(async (opts) => {
|
|
296
|
+
try {
|
|
297
|
+
const { execSync } = await import('child_process');
|
|
298
|
+
|
|
299
|
+
// 获取 staged 文件列表
|
|
300
|
+
let stagedFiles;
|
|
301
|
+
try {
|
|
302
|
+
stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' })
|
|
303
|
+
.trim().split('\n').filter(Boolean);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error('❌ 无法获取 git staged 文件(是否在 git 仓库中?)');
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (stagedFiles.length === 0) {
|
|
310
|
+
console.log('✅ No staged files');
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 过滤源文件
|
|
315
|
+
const { SOURCE_EXTS } = await import('../lib/service/guard/SourceFileCollector.js');
|
|
316
|
+
const { extname: _extname } = await import('path');
|
|
317
|
+
const sourceFiles = stagedFiles.filter(f => SOURCE_EXTS.has(_extname(f).toLowerCase()));
|
|
318
|
+
|
|
319
|
+
if (sourceFiles.length === 0) {
|
|
320
|
+
console.log('✅ No source files in staged changes');
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { bootstrap, container } = await initContainer();
|
|
325
|
+
const engine = container.get('guardCheckEngine');
|
|
326
|
+
const { detectLanguage } = await import('../lib/service/guard/GuardCheckEngine.js');
|
|
327
|
+
|
|
328
|
+
// 读取文件内容并检查
|
|
329
|
+
const files = [];
|
|
330
|
+
for (const f of sourceFiles) {
|
|
331
|
+
const filePath = resolve(f);
|
|
332
|
+
if (existsSync(filePath)) {
|
|
333
|
+
files.push({ path: filePath, content: readFileSync(filePath, 'utf8') });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const result = engine.auditFiles(files, { scope: 'file' });
|
|
338
|
+
const { summary } = result;
|
|
339
|
+
|
|
340
|
+
if (opts.json) {
|
|
341
|
+
console.log(JSON.stringify(result, null, 2));
|
|
342
|
+
} else if (summary.totalViolations === 0) {
|
|
343
|
+
console.log(`✅ ${summary.filesChecked} staged files passed Guard check`);
|
|
344
|
+
} else {
|
|
345
|
+
console.log(`\n🛡️ Guard check on ${summary.filesChecked} staged files:`);
|
|
346
|
+
console.log(` ${summary.totalErrors} errors, ${summary.totalViolations - summary.totalErrors} warnings\n`);
|
|
347
|
+
|
|
348
|
+
const filesWithIssues = result.files.filter(f => f.summary.total > 0);
|
|
349
|
+
for (const file of filesWithIssues.slice(0, 10)) {
|
|
350
|
+
console.log(` 📄 ${basename(file.filePath)} (${file.summary.errors}E / ${file.summary.warnings}W)`);
|
|
351
|
+
for (const v of file.violations.slice(0, 5)) {
|
|
352
|
+
const icon = v.severity === 'error' ? '❌' : '⚠️';
|
|
353
|
+
console.log(` ${icon} L${v.line} [${v.ruleId}] ${v.message}`);
|
|
354
|
+
}
|
|
355
|
+
if (file.violations.length > 5) console.log(` ... ${file.violations.length - 5} more`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await bootstrap.shutdown();
|
|
360
|
+
process.exit(summary.totalErrors > 0 ? 1 : 0);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.error('Error:', err.message);
|
|
363
|
+
if (process.env.ASD_DEBUG === '1') console.error(err.stack);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
224
368
|
// ─────────────────────────────────────────────────────
|
|
225
369
|
// watch 命令
|
|
226
370
|
// ─────────────────────────────────────────────────────
|
package/config/default.json
CHANGED
package/lib/core/AstAnalyzer.js
CHANGED
|
@@ -981,6 +981,181 @@ function _findIdentifier(node) {
|
|
|
981
981
|
return null;
|
|
982
982
|
}
|
|
983
983
|
|
|
984
|
+
// ──────────────────────────────────────────────────────────────────
|
|
985
|
+
// Guard AST 查询 API — 供 GuardCheckEngine AST 规则使用
|
|
986
|
+
// ──────────────────────────────────────────────────────────────────
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* 在 AST 中搜索特定调用表达式
|
|
990
|
+
* @param {string} source 源代码
|
|
991
|
+
* @param {string} lang 'objectivec' | 'swift'
|
|
992
|
+
* @param {string} targetCallee 目标调用,如 'URLSession.shared', 'dispatch_sync'
|
|
993
|
+
* @returns {Array<{ line: number, snippet: string, enclosingClass: string|null }>}
|
|
994
|
+
*/
|
|
995
|
+
function findCallExpressions(source, lang, targetCallee) {
|
|
996
|
+
const parser = _getParser(lang);
|
|
997
|
+
if (!parser) return [];
|
|
998
|
+
|
|
999
|
+
const tree = parser.parse(source);
|
|
1000
|
+
const results = [];
|
|
1001
|
+
const lines = source.split(/\r?\n/);
|
|
1002
|
+
|
|
1003
|
+
function walk(node, enclosingClass) {
|
|
1004
|
+
// 更新当前所处的类
|
|
1005
|
+
let currentClass = enclosingClass;
|
|
1006
|
+
if (['class_declaration', 'struct_declaration', 'class_interface', 'class_implementation'].includes(node.type)) {
|
|
1007
|
+
currentClass = _findIdentifier(node) || enclosingClass;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// 检查调用表达式
|
|
1011
|
+
const isCallLike = ['call_expression', 'message_expression', 'function_call_expression'].includes(node.type);
|
|
1012
|
+
if (isCallLike) {
|
|
1013
|
+
const nodeText = node.text || '';
|
|
1014
|
+
if (nodeText.includes(targetCallee)) {
|
|
1015
|
+
results.push({
|
|
1016
|
+
line: node.startPosition.row + 1,
|
|
1017
|
+
snippet: lines[node.startPosition.row]?.trim().slice(0, 120) || '',
|
|
1018
|
+
enclosingClass: currentClass,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// 对 Swift,也检查 member_access + call 的组合,如 URLSession.shared.data(...)
|
|
1024
|
+
if (node.type === 'navigation_expression' || node.type === 'member_expression') {
|
|
1025
|
+
const nodeText = node.text || '';
|
|
1026
|
+
if (nodeText.includes(targetCallee)) {
|
|
1027
|
+
// 只有当父节点是 call 时才算
|
|
1028
|
+
const parent = node.parent;
|
|
1029
|
+
if (parent && ['call_expression', 'function_call_expression'].includes(parent.type)) {
|
|
1030
|
+
// 已在 call_expression 中处理,跳过避免重复
|
|
1031
|
+
} else {
|
|
1032
|
+
results.push({
|
|
1033
|
+
line: node.startPosition.row + 1,
|
|
1034
|
+
snippet: lines[node.startPosition.row]?.trim().slice(0, 120) || '',
|
|
1035
|
+
enclosingClass: currentClass,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1042
|
+
walk(node.child(i), currentClass);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
walk(tree.rootNode, null);
|
|
1047
|
+
return results;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* 搜索特定模式在特定上下文中的出现
|
|
1052
|
+
* @param {string} source 源代码
|
|
1053
|
+
* @param {string} lang 'objectivec' | 'swift'
|
|
1054
|
+
* @param {string} pattern 要查找的文本模式(普通字符串匹配)
|
|
1055
|
+
* @param {{ forbiddenContext?: string, requiredContext?: string }} contextFilter
|
|
1056
|
+
* forbiddenContext: 如果在此上下文中出现则报告 (如 'dealloc')
|
|
1057
|
+
* requiredContext: 如果不在此上下文中出现则报告
|
|
1058
|
+
* @returns {Array<{ line: number, snippet: string, context: string|null }>}
|
|
1059
|
+
*/
|
|
1060
|
+
function findPatternInContext(source, lang, pattern, contextFilter = {}) {
|
|
1061
|
+
const parser = _getParser(lang);
|
|
1062
|
+
if (!parser) return [];
|
|
1063
|
+
|
|
1064
|
+
const tree = parser.parse(source);
|
|
1065
|
+
const results = [];
|
|
1066
|
+
const lines = source.split(/\r?\n/);
|
|
1067
|
+
|
|
1068
|
+
function getEnclosingMethodName(node) {
|
|
1069
|
+
let current = node.parent;
|
|
1070
|
+
while (current) {
|
|
1071
|
+
if (['method_definition', 'method_declaration', 'function_declaration', 'function_definition'].includes(current.type)) {
|
|
1072
|
+
return _findIdentifier(current) || null;
|
|
1073
|
+
}
|
|
1074
|
+
current = current.parent;
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function getEnclosingClassName(node) {
|
|
1080
|
+
let current = node.parent;
|
|
1081
|
+
while (current) {
|
|
1082
|
+
if (['class_declaration', 'struct_declaration', 'class_interface', 'class_implementation'].includes(current.type)) {
|
|
1083
|
+
return _findIdentifier(current) || null;
|
|
1084
|
+
}
|
|
1085
|
+
current = current.parent;
|
|
1086
|
+
}
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function walk(node) {
|
|
1091
|
+
const nodeText = node.text || '';
|
|
1092
|
+
if (nodeText.includes(pattern) && node.childCount === 0) {
|
|
1093
|
+
// 叶节点匹配
|
|
1094
|
+
const methodName = getEnclosingMethodName(node);
|
|
1095
|
+
const className = getEnclosingClassName(node);
|
|
1096
|
+
|
|
1097
|
+
if (contextFilter.forbiddenContext) {
|
|
1098
|
+
// 在禁止上下文中出现 → 报告
|
|
1099
|
+
if (methodName === contextFilter.forbiddenContext || className === contextFilter.forbiddenContext) {
|
|
1100
|
+
results.push({
|
|
1101
|
+
line: node.startPosition.row + 1,
|
|
1102
|
+
snippet: lines[node.startPosition.row]?.trim().slice(0, 120) || '',
|
|
1103
|
+
context: methodName || className,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
} else if (contextFilter.requiredContext) {
|
|
1107
|
+
// 不在要求的上下文中 → 报告
|
|
1108
|
+
if (className !== contextFilter.requiredContext && methodName !== contextFilter.requiredContext) {
|
|
1109
|
+
results.push({
|
|
1110
|
+
line: node.startPosition.row + 1,
|
|
1111
|
+
snippet: lines[node.startPosition.row]?.trim().slice(0, 120) || '',
|
|
1112
|
+
context: className || methodName,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1119
|
+
walk(node.child(i));
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
walk(tree.rootNode);
|
|
1124
|
+
return results;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* 检查类是否遵循指定协议
|
|
1129
|
+
* @param {string} source 源代码
|
|
1130
|
+
* @param {string} lang 'objectivec' | 'swift'
|
|
1131
|
+
* @param {string} className 类名
|
|
1132
|
+
* @param {string} protocolName 协议名
|
|
1133
|
+
* @returns {{ conforms: boolean, classFound: boolean, classDeclLine: number|null }}
|
|
1134
|
+
*/
|
|
1135
|
+
function checkProtocolConformance(source, lang, className, protocolName) {
|
|
1136
|
+
const summary = analyzeFile(source, lang);
|
|
1137
|
+
if (!summary) return { conforms: false, classFound: false, classDeclLine: null };
|
|
1138
|
+
|
|
1139
|
+
// 在 classes 中查找
|
|
1140
|
+
const cls = summary.classes.find(c => c.name === className);
|
|
1141
|
+
if (!cls) return { conforms: false, classFound: false, classDeclLine: null };
|
|
1142
|
+
|
|
1143
|
+
// 直接遵循
|
|
1144
|
+
if (cls.protocols?.includes(protocolName)) {
|
|
1145
|
+
return { conforms: true, classFound: true, classDeclLine: cls.line };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// 通过 extension/category 遵循
|
|
1149
|
+
const catConforms = summary.categories.some(
|
|
1150
|
+
cat => cat.className === className && cat.protocols?.includes(protocolName)
|
|
1151
|
+
);
|
|
1152
|
+
if (catConforms) {
|
|
1153
|
+
return { conforms: true, classFound: true, classDeclLine: cls.line };
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return { conforms: false, classFound: true, classDeclLine: cls.line };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
984
1159
|
// ──────────────────────────────────────────────────────────────────
|
|
985
1160
|
// 导出
|
|
986
1161
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -991,4 +1166,8 @@ export {
|
|
|
991
1166
|
generateContextForAgent,
|
|
992
1167
|
isAvailable,
|
|
993
1168
|
supportedLanguages,
|
|
1169
|
+
// Guard AST 查询 API
|
|
1170
|
+
findCallExpressions,
|
|
1171
|
+
findPatternInContext,
|
|
1172
|
+
checkProtocolConformance,
|
|
994
1173
|
};
|
|
@@ -65,7 +65,7 @@ export async function guardAuditFiles(ctx, args) {
|
|
|
65
65
|
|
|
66
66
|
const result = engine.auditFiles(filesToAudit, { scope });
|
|
67
67
|
|
|
68
|
-
// 写入 ViolationsStore
|
|
68
|
+
// 写入 ViolationsStore + GuardFeedbackLoop
|
|
69
69
|
try {
|
|
70
70
|
const violationsStore = ctx.container.get('violationsStore');
|
|
71
71
|
for (const fileResult of (result.files || [])) {
|
|
@@ -76,6 +76,12 @@ export async function guardAuditFiles(ctx, args) {
|
|
|
76
76
|
summary: `MCP audit (${scope}): ${fileResult.summary.errors}E ${fileResult.summary.warnings}W`,
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
// Guard ↔ Recipe 闭环:检测修复并自动确认使用
|
|
81
|
+
try {
|
|
82
|
+
const feedbackLoop = ctx.container.get('guardFeedbackLoop');
|
|
83
|
+
feedbackLoop.processFixDetection(fileResult, fileResult.filePath);
|
|
84
|
+
} catch { /* guardFeedbackLoop not available */ }
|
|
79
85
|
}
|
|
80
86
|
} catch { /* ViolationsStore not available */ }
|
|
81
87
|
|
|
@@ -291,4 +291,31 @@ router.post('/import-from-recipe', asyncHandler(async (req, res) => {
|
|
|
291
291
|
});
|
|
292
292
|
}));
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* GET /api/v1/rules/compliance
|
|
296
|
+
* 生成全项目合规报告
|
|
297
|
+
* Query params:
|
|
298
|
+
* - path: 扫描根目录(默认 projectRoot)
|
|
299
|
+
* - maxErrors: Quality Gate 最大 error 数(默认 0)
|
|
300
|
+
* - maxWarnings: Quality Gate 最大 warning 数(默认 20)
|
|
301
|
+
* - minScore: Quality Gate 最低分(默认 70)
|
|
302
|
+
* - maxFiles: 最大扫描文件数(默认 500)
|
|
303
|
+
*/
|
|
304
|
+
router.get('/compliance', asyncHandler(async (req, res) => {
|
|
305
|
+
const container = getServiceContainer();
|
|
306
|
+
const reporter = container.get('complianceReporter');
|
|
307
|
+
const projectRoot = req.query.path || process.env.ASD_PROJECT_DIR || process.cwd();
|
|
308
|
+
|
|
309
|
+
const report = await reporter.generate(projectRoot, {
|
|
310
|
+
qualityGate: {
|
|
311
|
+
maxErrors: parseInt(req.query.maxErrors) || 0,
|
|
312
|
+
maxWarnings: parseInt(req.query.maxWarnings) || 20,
|
|
313
|
+
minScore: parseInt(req.query.minScore) || 70,
|
|
314
|
+
},
|
|
315
|
+
maxFiles: parseInt(req.query.maxFiles) || 500,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
res.json({ success: true, data: report });
|
|
319
|
+
}));
|
|
320
|
+
|
|
294
321
|
export default router;
|