@spaceflow/review 0.83.0 → 1.0.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/src/mcp/index.ts CHANGED
@@ -2,7 +2,7 @@ import { t, z, type SpaceflowContext, type GitProviderService } from "@spaceflow
2
2
  import { ReviewSpecService } from "../review-spec";
3
3
  import { ChangedFileCollection } from "../changed-file-collection";
4
4
  import type { ReviewConfig } from "../review.config";
5
- import { extractGlobsFromIncludes } from "../review-includes-filter";
5
+ import { matchIncludes } from "../review-includes-filter";
6
6
  import { join } from "path";
7
7
  import { existsSync } from "fs";
8
8
 
@@ -115,16 +115,11 @@ export const tools = [
115
115
  allSpecs,
116
116
  ChangedFileCollection.from([{ filename: filePath }]),
117
117
  );
118
- const micromatchModule = await import("micromatch");
119
- const micromatch = micromatchModule.default || micromatchModule;
120
118
  const rules = applicableSpecs.flatMap((spec) =>
121
119
  spec.rules
122
120
  .filter((rule) => {
123
121
  const includes = rule.includes || spec.includes;
124
- if (includes.length === 0) return true;
125
- const globs = extractGlobsFromIncludes(includes);
126
- if (globs.length === 0) return true;
127
- return micromatch.isMatch(filePath, globs, { matchBase: true });
122
+ return matchIncludes(includes, filePath);
128
123
  })
129
124
  .map((rule) => ({
130
125
  id: rule.id,
@@ -135,9 +130,13 @@ export const tools = [
135
130
  ...(includeExamples && rule.examples.length > 0
136
131
  ? {
137
132
  examples: rule.examples.map((ex) => ({
138
- type: ex.type,
139
- lang: ex.lang,
140
- code: ex.code,
133
+ title: ex.title,
134
+ description: ex.description,
135
+ content: ex.content.map((c) => ({
136
+ title: c.title,
137
+ type: c.type,
138
+ description: c.description,
139
+ })),
141
140
  })),
142
141
  }
143
142
  : {}),
@@ -170,9 +169,13 @@ export const tools = [
170
169
  includes: spec.includes,
171
170
  overrides: rule.overrides,
172
171
  examples: rule.examples.map((ex) => ({
173
- type: ex.type,
174
- lang: ex.lang,
175
- code: ex.code,
172
+ title: ex.title,
173
+ description: ex.description,
174
+ content: ex.content.map((c) => ({
175
+ title: c.title,
176
+ type: c.type,
177
+ description: c.description,
178
+ })),
176
179
  })),
177
180
  };
178
181
  },
@@ -209,9 +212,13 @@ export const tools = [
209
212
  ...(includeExamples && rule.examples.length > 0
210
213
  ? {
211
214
  examples: rule.examples.map((ex) => ({
212
- type: ex.type,
213
- lang: ex.lang,
214
- code: ex.code,
215
+ title: ex.title,
216
+ description: ex.description,
217
+ content: ex.content.map((c) => ({
218
+ title: c.title,
219
+ type: c.type,
220
+ description: c.description,
221
+ })),
215
222
  })),
216
223
  }
217
224
  : { hasExamples: rule.examples.length > 0 }),
@@ -0,0 +1,47 @@
1
+ import type { ReviewSpec, RuleExample, RuleContent } from "../review-spec/types";
2
+
3
+ /**
4
+ * 构建 specs 的 prompt 部分
5
+ */
6
+ export function buildSpecsSection(specs: ReviewSpec[]): string {
7
+ return specs
8
+ .map((spec) => {
9
+ const firstRule = spec.rules[0];
10
+ const rulesText = spec.rules
11
+ .slice(1)
12
+ .map((rule) => {
13
+ let text = `#### [${rule.id}] ${rule.title}\n`;
14
+ if (rule.description) {
15
+ text += `${rule.description}\n`;
16
+ }
17
+ if (rule.examples.length > 0) {
18
+ for (const example of rule.examples) {
19
+ text += formatExample(example);
20
+ }
21
+ }
22
+ return text;
23
+ })
24
+ .join("\n");
25
+
26
+ return `### ${firstRule.title}\n- 规范文件: ${spec.filename}\n- 适用扩展名: ${spec.extensions.join(", ")}\n\n${rulesText}`;
27
+ })
28
+ .join("\n\n-------------------\n\n");
29
+ }
30
+
31
+ function formatExample(example: RuleExample): string {
32
+ let text = "";
33
+ if (example.title) {
34
+ text += `##### ${example.title}\n`;
35
+ }
36
+ if (example.description) {
37
+ text += `${example.description}\n`;
38
+ }
39
+ for (const item of example.content) {
40
+ text += formatContent(item);
41
+ }
42
+ return text;
43
+ }
44
+
45
+ function formatContent(item: RuleContent): string {
46
+ return `###### ${item.type}${item.title ? `: ${item.title}` : ""}\n${item.description}\n`;
47
+ }
@@ -4,6 +4,7 @@ import {
4
4
  filterFilesByIncludes,
5
5
  extractGlobsFromIncludes,
6
6
  extractCodeBlockTypes,
7
+ matchIncludes,
7
8
  } from "./review-includes-filter";
8
9
 
9
10
  describe("review-includes-filter", () => {
@@ -281,4 +282,86 @@ describe("review-includes-filter", () => {
281
282
  expect(extractCodeBlockTypes([])).toEqual([]);
282
283
  });
283
284
  });
285
+
286
+ describe("matchIncludes", () => {
287
+ const glob = "**/*.ts";
288
+
289
+ it("includes 为空时返回 true", () => {
290
+ expect(matchIncludes([], "src/foo.ts")).toBe(true);
291
+ });
292
+
293
+ it("filename 为空时返回 false", () => {
294
+ expect(matchIncludes([glob], "")).toBe(false);
295
+ });
296
+
297
+ it("不传 fileStatus 时降级为纯 glob 匹配", () => {
298
+ expect(matchIncludes([glob], "src/foo.ts")).toBe(true);
299
+ expect(matchIncludes([glob], "src/foo.vue")).toBe(false);
300
+ });
301
+
302
+ it("不传 fileStatus 时 status 前缀降级为纯 glob 匹配", () => {
303
+ // added|**/*.ts 在无 status 信息时,降级为 glob **/*.ts 匹配
304
+ expect(matchIncludes([`added|${glob}`], "src/foo.ts")).toBe(true);
305
+ expect(matchIncludes([`added|${glob}`], "src/foo.vue")).toBe(false);
306
+ });
307
+
308
+ it("无前缀 glob 不限 status,匹配所有符合的文件", () => {
309
+ expect(matchIncludes([glob], "src/foo.ts", "added")).toBe(true);
310
+ expect(matchIncludes([glob], "src/foo.ts", "modified")).toBe(true);
311
+ expect(matchIncludes([glob], "src/foo.ts", "removed")).toBe(true);
312
+ });
313
+
314
+ it("added| 前缀只匹配 added 状态文件", () => {
315
+ expect(matchIncludes([`added|${glob}`], "src/foo.ts", "added")).toBe(true);
316
+ expect(matchIncludes([`added|${glob}`], "src/foo.ts", "modified")).toBe(false);
317
+ });
318
+
319
+ it("modified| 前缀只匹配 modified 状态文件", () => {
320
+ expect(matchIncludes([`modified|${glob}`], "src/foo.ts", "modified")).toBe(true);
321
+ expect(matchIncludes([`modified|${glob}`], "src/foo.ts", "added")).toBe(false);
322
+ });
323
+
324
+ it("deleted| 前缀匹配 removed 和 deleted 状态文件", () => {
325
+ expect(matchIncludes([`deleted|${glob}`], "src/old.ts", "removed")).toBe(true);
326
+ expect(matchIncludes([`deleted|${glob}`], "src/old.ts", "deleted")).toBe(true);
327
+ expect(matchIncludes([`deleted|${glob}`], "src/old.ts", "modified")).toBe(false);
328
+ });
329
+
330
+ it("排除模式 ! 优先过滤", () => {
331
+ expect(matchIncludes([glob, "!**/*.spec.ts"], "src/foo.spec.ts", "added")).toBe(false);
332
+ expect(matchIncludes([glob, "!**/*.spec.ts"], "src/foo.ts", "added")).toBe(true);
333
+ });
334
+
335
+ it("多个 status 前缀之间是 OR 关系", () => {
336
+ expect(matchIncludes([`added|${glob}`, `modified|${glob}`], "src/foo.ts", "added")).toBe(
337
+ true,
338
+ );
339
+ expect(matchIncludes([`added|${glob}`, `modified|${glob}`], "src/foo.ts", "modified")).toBe(
340
+ true,
341
+ );
342
+ });
343
+
344
+ it("无前缀 glob 与 status 前缀混用时任一命中即保留", () => {
345
+ expect(matchIncludes([glob, "added|**/*.vue"], "src/foo.ts", "modified")).toBe(true);
346
+ expect(matchIncludes([glob, "added|**/*.vue"], "src/foo.vue", "added")).toBe(true);
347
+ expect(matchIncludes([glob, "added|**/*.vue"], "src/foo.vue", "modified")).toBe(false);
348
+ });
349
+
350
+ it("status 内排除语法 added|!**/*.spec.ts", () => {
351
+ expect(matchIncludes([`added|${glob}`, "added|!**/*.spec.ts"], "src/foo.ts", "added")).toBe(
352
+ true,
353
+ );
354
+ expect(
355
+ matchIncludes([`added|${glob}`, "added|!**/*.spec.ts"], "src/foo.spec.ts", "added"),
356
+ ).toBe(false);
357
+ });
358
+
359
+ it("fileStatus 为 undefined 时走降级路径(纯 glob 匹配)", () => {
360
+ // matchIncludes 中 undefined 表示无 status 信息,降级为纯 glob 匹配
361
+ // 这与 filterFilesByIncludes 中 status=undefined fallback 为 modified 不同
362
+ // 因为 matchIncludes 用于 spec includes 场景,无 status 时应宽松匹配
363
+ expect(matchIncludes([`modified|${glob}`], "src/foo.ts")).toBe(true);
364
+ expect(matchIncludes([`added|${glob}`], "src/foo.ts")).toBe(true);
365
+ });
366
+ });
284
367
  });
@@ -180,6 +180,86 @@ export function extractGlobsFromIncludes(includes: string[]): string[] {
180
180
  return includes.map((p) => parseIncludePattern(p).glob).filter((g) => g.length > 0);
181
181
  }
182
182
 
183
+ /**
184
+ * 检查单个文件是否匹配 includes 模式列表,支持 `status|glob` 前缀语法。
185
+ *
186
+ * - 当 `fileStatus` 未提供时,status 前缀的 includes 降级为纯 glob 匹配(向后兼容)
187
+ * - 当 `fileStatus` 提供时,完整支持 `added|`/`modified|`/`deleted|` 前缀语义
188
+ *
189
+ * 算法与 `filterFilesByIncludes` 一致,但针对单文件场景优化:
190
+ * 1. 排除模式(`!`) 优先过滤
191
+ * 2. 无前缀正向 glob 匹配
192
+ * 3. 有 status 前缀的 glob 按文件实际 status 过滤
193
+ *
194
+ * @param includes include 模式列表
195
+ * @param filename 待匹配的文件名
196
+ * @param fileStatus 文件变更状态(如 "added"/"modified"/"removed"),不提供时降级为纯 glob
197
+ * @returns 是否匹配
198
+ */
199
+ export function matchIncludes(includes: string[], filename: string, fileStatus?: string): boolean {
200
+ if (!includes || includes.length === 0) return true;
201
+ if (!filename) return false;
202
+
203
+ const parsed = includes.map(parseIncludePattern);
204
+
205
+ // 无 status 信息时降级为纯 glob 匹配(向后兼容)
206
+ if (!fileStatus) {
207
+ const globs = extractGlobsFromIncludes(includes);
208
+ if (globs.length === 0) return true;
209
+ return micromatch.isMatch(filename, globs, { matchBase: true });
210
+ }
211
+
212
+ const normalizedStatus = STATUS_ALIAS[fileStatus.toLowerCase()] ?? "modified";
213
+
214
+ // 排除模式(以 ! 开头),用于最终全局过滤
215
+ const negativeGlobs = parsed
216
+ .filter((p) => p.status === undefined && p.glob.startsWith("!"))
217
+ .map((p) => p.glob.slice(1));
218
+ // 无前缀的正向 globs
219
+ const plainGlobs = parsed
220
+ .filter((p) => p.status === undefined && !p.glob.startsWith("!"))
221
+ .map((p) => p.glob);
222
+ // 有 status 前缀的 patterns
223
+ const statusPatterns = parsed.filter((p) => p.status !== undefined);
224
+
225
+ // 最终排除:命中排除模式的文件直接过滤掉
226
+ if (
227
+ negativeGlobs.length > 0 &&
228
+ micromatch.isMatch(filename, negativeGlobs, { matchBase: true })
229
+ ) {
230
+ return false;
231
+ }
232
+
233
+ // 正向匹配:无前缀 glob
234
+ if (plainGlobs.length > 0 && micromatch.isMatch(filename, plainGlobs, { matchBase: true })) {
235
+ return true;
236
+ }
237
+
238
+ // 正向匹配:有 status 前缀的 glob,按文件实际 status 过滤
239
+ if (statusPatterns.length > 0) {
240
+ const matchingStatusGlobs = statusPatterns
241
+ .filter(({ status }) => status === normalizedStatus)
242
+ .map(({ glob }) => glob);
243
+ if (matchingStatusGlobs.length > 0) {
244
+ const positiveGlobs = matchingStatusGlobs.filter((g) => !g.startsWith("!"));
245
+ const negativeStatusGlobs = matchingStatusGlobs
246
+ .filter((g) => g.startsWith("!"))
247
+ .map((g) => g.slice(1));
248
+ if (positiveGlobs.length > 0) {
249
+ const matchesPositive = micromatch.isMatch(filename, positiveGlobs, { matchBase: true });
250
+ const matchesNegative =
251
+ negativeStatusGlobs.length > 0 &&
252
+ micromatch.isMatch(filename, negativeStatusGlobs, { matchBase: true });
253
+ if (matchesPositive && !matchesNegative) {
254
+ return true;
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ return false;
261
+ }
262
+
183
263
  /**
184
264
  * 从 whenModifiedCode 配置中解析代码结构过滤类型。
185
265
  * 只接受简单的类型名称,如 "function"、"class"、"interface"、"type"、"method"
package/src/review-llm.ts CHANGED
@@ -19,11 +19,10 @@ import {
19
19
  } from "./review-spec";
20
20
  import { readdir } from "fs/promises";
21
21
  import { dirname, extname } from "path";
22
- import micromatch from "micromatch";
23
22
  import type { FileReviewPrompt, ReviewPrompt, LLMReviewOptions } from "./types/review-llm";
24
23
  import { buildLinesWithNumbers, buildCommitsSection, extractCodeBlocks } from "./utils/review-llm";
25
24
  import { ChangedFileCollection } from "./changed-file-collection";
26
- import { extractCodeBlockTypes, extractGlobsFromIncludes } from "./review-includes-filter";
25
+ import { matchIncludes, extractCodeBlockTypes } from "./review-includes-filter";
27
26
  import {
28
27
  REVIEW_SCHEMA,
29
28
  buildFileReviewPrompt,
@@ -63,8 +62,9 @@ export class ReviewLlmProcessor {
63
62
  * 根据文件过滤 specs,只返回与该文件匹配的规则
64
63
  * - 如果 spec 有 includes 配置,只有当文件名匹配 includes 模式时才包含该 spec
65
64
  * - 如果 spec 没有 includes 配置,则按扩展名匹配
65
+ * - 支持 `added|`/`modified|`/`deleted|` 前缀语法,需传入 fileStatus
66
66
  */
67
- filterSpecsForFile(specs: ReviewSpec[], filename: string): ReviewSpec[] {
67
+ filterSpecsForFile(specs: ReviewSpec[], filename: string, fileStatus?: string): ReviewSpec[] {
68
68
  const ext = extname(filename).slice(1).toLowerCase();
69
69
  if (!ext) return [];
70
70
 
@@ -75,11 +75,9 @@ export class ReviewLlmProcessor {
75
75
  }
76
76
 
77
77
  // 如果有 includes 配置,检查文件名是否匹配 includes 模式
78
- // 需先提取纯 glob(去掉 added|/modified| 前缀,过滤 code-* 空串),避免 micromatch 报错
78
+ // 使用 matchIncludes 支持 status|glob 前缀语法
79
79
  if (spec.includes.length > 0) {
80
- const globs = extractGlobsFromIncludes(spec.includes);
81
- if (globs.length === 0) return true;
82
- return micromatch.isMatch(filename, globs, { matchBase: true });
80
+ return matchIncludes(spec.includes, filename, fileStatus);
83
81
  }
84
82
 
85
83
  // 没有 includes 配置,扩展名匹配即可
@@ -124,7 +122,7 @@ export class ReviewLlmProcessor {
124
122
  const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
125
123
 
126
124
  // 根据文件过滤 specs,只注入与当前文件匹配的规则
127
- const fileSpecs = this.filterSpecsForFile(specs, filename);
125
+ const fileSpecs = this.filterSpecsForFile(specs, filename, file.status);
128
126
 
129
127
  // 从全局 whenModifiedCode 配置中解析代码结构过滤类型
130
128
  const codeBlockTypes = whenModifiedCode ? extractCodeBlockTypes(whenModifiedCode) : [];
@@ -35,7 +35,7 @@ describe("ReviewSpecService", () => {
35
35
  - 排除配置文件
36
36
  - 排除测试文件
37
37
 
38
- ### Good
38
+ #### Good: 合理的常量命名
39
39
  \`\`\`js
40
40
  const MAX_COUNT = 100;
41
41
  \`\`\``;
@@ -59,10 +59,13 @@ const MAX_COUNT = 100;
59
59
  expect(specs[0].rules[1].description).toContain("排除配置文件");
60
60
  expect(specs[0].rules[1].description).toContain("排除测试文件");
61
61
  expect(specs[0].rules[1].examples).toHaveLength(1);
62
- expect(specs[0].rules[1].examples[0]).toEqual({
63
- lang: "js",
64
- code: "const MAX_COUNT = 100;",
62
+ expect(specs[0].rules[1].examples[0].title).toBe("");
63
+ expect(specs[0].rules[1].examples[0].description).toBe("");
64
+ expect(specs[0].rules[1].examples[0].content).toHaveLength(1);
65
+ expect(specs[0].rules[1].examples[0].content[0]).toEqual({
66
+ title: "合理的常量命名",
65
67
  type: "good",
68
+ description: "const MAX_COUNT = 100;",
66
69
  });
67
70
 
68
71
  expect(specs[1].filename).toBe("vue.file-name.md");
@@ -309,7 +312,7 @@ const MAX_COUNT = 100;
309
312
 
310
313
  > - severity \`warn\`
311
314
 
312
- ### Good
315
+ #### Good: 合理的常量命名
313
316
  \`\`\`js
314
317
  const MAX_COUNT = 100;
315
318
  \`\`\``;
@@ -566,6 +569,92 @@ const MAX_COUNT = 100;
566
569
 
567
570
  expect(result).toHaveLength(2);
568
571
  });
572
+
573
+ it("should filter issues by spec includes with added| prefix when fileStatusMap provided", () => {
574
+ const specs = [
575
+ {
576
+ filename: "js.models.md",
577
+ extensions: ["js"],
578
+ type: "models",
579
+ content: "",
580
+ overrides: [],
581
+ severity: "error" as const,
582
+ includes: ["added|*.model.js"],
583
+ rules: [
584
+ { id: "Js.Models", title: "Models", description: "", examples: [], overrides: [] },
585
+ ],
586
+ },
587
+ ];
588
+
589
+ const issues = [
590
+ { file: "user/models/user.model.js", ruleId: "Js.Models.Rule1", reason: "test" },
591
+ { file: "user/models/product.model.js", ruleId: "Js.Models.Rule2", reason: "test" },
592
+ ];
593
+
594
+ // added 文件保留,modified 文件被 added| 前缀过滤
595
+ const changedFiles = ChangedFileCollection.from([
596
+ { filename: "user/models/user.model.js", status: "added" },
597
+ { filename: "user/models/product.model.js", status: "modified" },
598
+ ]);
599
+ const result = service.filterIssuesByIncludes(issues, specs, changedFiles);
600
+ expect(result).toHaveLength(1);
601
+ expect(result[0].file).toBe("user/models/user.model.js");
602
+ });
603
+
604
+ it("should fall back to pure glob matching when fileStatusMap not provided", () => {
605
+ const specs = [
606
+ {
607
+ filename: "js.models.md",
608
+ extensions: ["js"],
609
+ type: "models",
610
+ content: "",
611
+ overrides: [],
612
+ severity: "error" as const,
613
+ includes: ["added|*.model.js"],
614
+ rules: [
615
+ { id: "Js.Models", title: "Models", description: "", examples: [], overrides: [] },
616
+ ],
617
+ },
618
+ ];
619
+
620
+ const issues = [
621
+ { file: "user/models/user.model.js", ruleId: "Js.Models.Rule1", reason: "test" },
622
+ { file: "src/app.js", ruleId: "Js.Models.Rule2", reason: "test" },
623
+ ];
624
+
625
+ // 无 status map 时,added| 前缀降级为纯 glob 匹配
626
+ const result = service.filterIssuesByIncludes(issues, specs);
627
+ expect(result).toHaveLength(1);
628
+ expect(result[0].file).toBe("user/models/user.model.js");
629
+ });
630
+
631
+ it("should filter issues by spec includes with modified| prefix", () => {
632
+ const specs = [
633
+ {
634
+ filename: "js.nest.md",
635
+ extensions: ["ts"],
636
+ type: "nest",
637
+ content: "",
638
+ overrides: [],
639
+ severity: "error" as const,
640
+ includes: ["modified|*.controller.ts"],
641
+ rules: [{ id: "JsTs.Nest", title: "Nest", description: "", examples: [], overrides: [] }],
642
+ },
643
+ ];
644
+
645
+ const issues = [
646
+ { file: "user.controller.ts", ruleId: "JsTs.Nest.Rule1", reason: "test" },
647
+ { file: "app.controller.ts", ruleId: "JsTs.Nest.Rule2", reason: "test" },
648
+ ];
649
+
650
+ const changedFiles = ChangedFileCollection.from([
651
+ { filename: "user.controller.ts", status: "modified" },
652
+ { filename: "app.controller.ts", status: "added" },
653
+ ]);
654
+ const result = service.filterIssuesByIncludes(issues, specs, changedFiles);
655
+ expect(result).toHaveLength(1);
656
+ expect(result[0].file).toBe("user.controller.ts");
657
+ });
569
658
  });
570
659
 
571
660
  describe("matchRuleId", () => {
@@ -714,7 +803,13 @@ const MAX_COUNT = 100;
714
803
  id: "JsTs.Base.Rule1",
715
804
  title: "Rule1",
716
805
  description: "rule desc",
717
- examples: [{ lang: "ts", code: "const x = 1;", type: "good" as const }],
806
+ examples: [
807
+ {
808
+ title: "",
809
+ description: "",
810
+ content: [{ title: "", type: "good" as const, description: "const x = 1;" }],
811
+ },
812
+ ],
718
813
  overrides: [],
719
814
  },
720
815
  ],
@@ -723,7 +818,7 @@ const MAX_COUNT = 100;
723
818
  const result = service.buildSpecsSection(specs);
724
819
  expect(result).toContain("基础规范");
725
820
  expect(result).toContain("Rule1");
726
- expect(result).toContain("推荐做法");
821
+ expect(result).toContain("good");
727
822
  });
728
823
 
729
824
  it("should handle rules without examples", () => {
@@ -1163,7 +1258,7 @@ const MAX_COUNT = 100;
1163
1258
  ];
1164
1259
  const issues = [{ ruleId: "JsTs.FileName", file: "src/app.ts", line: "10" }];
1165
1260
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1166
- const result = service.filterIssuesByOverrides(issues, specs, 1);
1261
+ const result = service.filterIssuesByOverrides(issues, specs, undefined, 1);
1167
1262
  expect(result).toHaveLength(0);
1168
1263
  expect(consoleSpy).toHaveBeenCalled();
1169
1264
  consoleSpy.mockRestore();
@@ -1192,7 +1287,7 @@ const MAX_COUNT = 100;
1192
1287
  ];
1193
1288
  const issues = [{ ruleId: "Other.Rule", file: "src/app.ts" }];
1194
1289
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1195
- service.filterIssuesByOverrides(issues, specs, 3);
1290
+ service.filterIssuesByOverrides(issues, specs, undefined, 3);
1196
1291
  expect(consoleSpy).toHaveBeenCalled();
1197
1292
  consoleSpy.mockRestore();
1198
1293
  });
@@ -1558,14 +1653,16 @@ const MAX_COUNT = 100;
1558
1653
 
1559
1654
  describe("extractExamples - bad type", () => {
1560
1655
  it("should extract bad examples", () => {
1561
- const content = `### Bad
1656
+ const content = `#### Bad: 不合理的命名
1562
1657
 
1563
1658
  \`\`\`ts
1564
1659
  const bad_name = 1;
1565
1660
  \`\`\``;
1566
1661
  const examples = (service as any).extractExamples(content);
1567
1662
  expect(examples).toHaveLength(1);
1568
- expect(examples[0].type).toBe("bad");
1663
+ expect(examples[0].content).toHaveLength(1);
1664
+ expect(examples[0].content[0].type).toBe("bad");
1665
+ expect(examples[0].content[0].title).toBe("不合理的命名");
1569
1666
  });
1570
1667
  });
1571
1668
 
@@ -1617,4 +1714,125 @@ const bad_name = 1;
1617
1714
  expect(result[0].file).toBe("user.controller.ts");
1618
1715
  });
1619
1716
  });
1717
+
1718
+ describe("extractExamples - #### level with colon", () => {
1719
+ it("should extract examples from #### Good: / #### Bad: format without group", () => {
1720
+ const content = `#### Good: 合理的常量命名
1721
+
1722
+ \`\`\`javascript
1723
+ const MAX_COUNT = 100;
1724
+ \`\`\`
1725
+
1726
+ #### Bad: 不合理的常量命名
1727
+
1728
+ \`\`\`javascript
1729
+ const maxCount = 100;
1730
+ \`\`\``;
1731
+ const examples = (service as any).extractExamples(content);
1732
+ expect(examples).toHaveLength(1);
1733
+ expect(examples[0].title).toBe("");
1734
+ expect(examples[0].description).toBe("");
1735
+ expect(examples[0].content).toHaveLength(2);
1736
+ expect(examples[0].content[0]).toEqual({
1737
+ title: "合理的常量命名",
1738
+ type: "good",
1739
+ description: "const MAX_COUNT = 100;",
1740
+ });
1741
+ expect(examples[0].content[1]).toEqual({
1742
+ title: "不合理的常量命名",
1743
+ type: "bad",
1744
+ description: "const maxCount = 100;",
1745
+ });
1746
+ });
1747
+
1748
+ it("should parse full rule with #### examples", () => {
1749
+ const mockContent = `# 基础代码规范 \`[JsTs.Base]\`
1750
+
1751
+ ## 常量名使用大写加下划线命名 \`[JsTs.Base.ConstUpperCase]\`
1752
+
1753
+ - 不检查 nodejs 的导包定义
1754
+ - 常量检查只需检查 const 声明的静态值
1755
+
1756
+ #### Good: 合理的常量命名
1757
+
1758
+ \`\`\`javascript
1759
+ const MAX_COUNT = 100;
1760
+ \`\`\`
1761
+
1762
+ #### Bad: 不合理的常量命名
1763
+
1764
+ \`\`\`javascript
1765
+ const maxCount = 100;
1766
+ \`\`\``;
1767
+
1768
+ const spec = service.parseSpecFile("js&ts.base.md", mockContent);
1769
+ expect(spec).not.toBeNull();
1770
+ expect(spec!.rules).toHaveLength(2);
1771
+ expect(spec!.rules[1].id).toBe("JsTs.Base.ConstUpperCase");
1772
+ expect(spec!.rules[1].description).toContain("不检查 nodejs");
1773
+ expect(spec!.rules[1].description).not.toContain("Good");
1774
+ expect(spec!.rules[1].examples).toHaveLength(1);
1775
+ expect(spec!.rules[1].examples[0].content).toHaveLength(2);
1776
+ expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
1777
+ expect(spec!.rules[1].examples[0].content[1].type).toBe("bad");
1778
+ });
1779
+
1780
+ it("should parse multiple example groups with ### Example:", () => {
1781
+ const mockContent = `# 基础代码规范 \`[JsTs.Base]\`
1782
+
1783
+ ## 常量名使用大写加下划线命名 \`[JsTs.Base.ConstUpperCase]\`
1784
+
1785
+ - 不检查 nodejs 的导包定义
1786
+ - 常量检查只需检查 const 声明的静态值
1787
+
1788
+ ### Example: 下面的明明规则说明
1789
+
1790
+ #### Good: 合理的常量命名
1791
+
1792
+ \`\`\`javascript
1793
+ const MAX_COUNT = 100;
1794
+ \`\`\`
1795
+
1796
+ #### Bad: 不合理的常量命名
1797
+
1798
+ \`\`\`javascript
1799
+ const maxCount = 100;
1800
+ \`\`\`
1801
+
1802
+ ### Example: 另一种场景
1803
+
1804
+ #### Good: 枚举值命名
1805
+
1806
+ \`\`\`javascript
1807
+ const STATUS_ACTIVE = "active";
1808
+ \`\`\`
1809
+
1810
+ #### Bad: 枚举值小驼峰
1811
+
1812
+ \`\`\`javascript
1813
+ const statusActive = "active";
1814
+ \`\`\``;
1815
+
1816
+ const spec = service.parseSpecFile("js&ts.base.md", mockContent);
1817
+ expect(spec).not.toBeNull();
1818
+ expect(spec!.rules[1].id).toBe("JsTs.Base.ConstUpperCase");
1819
+ expect(spec!.rules[1].description).toContain("不检查 nodejs");
1820
+ expect(spec!.rules[1].description).not.toContain("Example");
1821
+ expect(spec!.rules[1].examples).toHaveLength(2);
1822
+
1823
+ // 第一组
1824
+ expect(spec!.rules[1].examples[0].title).toBe("Example");
1825
+ expect(spec!.rules[1].examples[0].description).toBe("下面的明明规则说明");
1826
+ expect(spec!.rules[1].examples[0].content).toHaveLength(2);
1827
+ expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
1828
+ expect(spec!.rules[1].examples[0].content[1].type).toBe("bad");
1829
+
1830
+ // 第二组
1831
+ expect(spec!.rules[1].examples[1].title).toBe("Example");
1832
+ expect(spec!.rules[1].examples[1].description).toBe("另一种场景");
1833
+ expect(spec!.rules[1].examples[1].content).toHaveLength(2);
1834
+ expect(spec!.rules[1].examples[1].content[0].type).toBe("good");
1835
+ expect(spec!.rules[1].examples[1].content[1].type).toBe("bad");
1836
+ });
1837
+ });
1620
1838
  });