@aigne/doc-smith 0.6.0 → 0.7.1

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 (80) hide show
  1. package/.aigne/doc-smith/config.yaml +70 -0
  2. package/.aigne/doc-smith/output/structure-plan.json +152 -0
  3. package/.aigne/doc-smith/preferences.yml +31 -0
  4. package/.aigne/doc-smith/upload-cache.yaml +288 -0
  5. package/.github/workflows/ci.yml +46 -0
  6. package/.github/workflows/reviewer.yml +2 -1
  7. package/CHANGELOG.md +17 -0
  8. package/README.md +33 -15
  9. package/agents/chat.yaml +30 -0
  10. package/agents/check-structure-plan.mjs +1 -1
  11. package/agents/docs-fs.yaml +25 -0
  12. package/agents/exit.mjs +6 -0
  13. package/agents/feedback-refiner.yaml +5 -1
  14. package/agents/find-items-by-paths.mjs +10 -4
  15. package/agents/fs.mjs +60 -0
  16. package/agents/input-generator.mjs +150 -91
  17. package/agents/load-config.mjs +0 -5
  18. package/agents/load-sources.mjs +61 -8
  19. package/agents/publish-docs.mjs +27 -12
  20. package/agents/retranslate.yaml +1 -1
  21. package/agents/team-publish-docs.yaml +2 -2
  22. package/aigne.yaml +1 -0
  23. package/docs/_sidebar.md +17 -0
  24. package/docs/advanced-how-it-works.md +104 -0
  25. package/docs/advanced-how-it-works.zh.md +104 -0
  26. package/docs/advanced-quality-assurance.md +64 -0
  27. package/docs/advanced-quality-assurance.zh.md +64 -0
  28. package/docs/advanced.md +28 -0
  29. package/docs/advanced.zh.md +28 -0
  30. package/docs/changelog.md +272 -0
  31. package/docs/changelog.zh.md +272 -0
  32. package/docs/cli-reference.md +185 -0
  33. package/docs/cli-reference.zh.md +185 -0
  34. package/docs/configuration-interactive-setup.md +82 -0
  35. package/docs/configuration-interactive-setup.zh.md +82 -0
  36. package/docs/configuration-language-support.md +64 -0
  37. package/docs/configuration-language-support.zh.md +64 -0
  38. package/docs/configuration-llm-setup.md +90 -0
  39. package/docs/configuration-llm-setup.zh.md +90 -0
  40. package/docs/configuration-preferences.md +122 -0
  41. package/docs/configuration-preferences.zh.md +123 -0
  42. package/docs/configuration.md +173 -0
  43. package/docs/configuration.zh.md +173 -0
  44. package/docs/features-generate-documentation.md +82 -0
  45. package/docs/features-generate-documentation.zh.md +82 -0
  46. package/docs/features-publish-your-docs.md +98 -0
  47. package/docs/features-publish-your-docs.zh.md +98 -0
  48. package/docs/features-translate-documentation.md +83 -0
  49. package/docs/features-translate-documentation.zh.md +83 -0
  50. package/docs/features-update-and-refine.md +86 -0
  51. package/docs/features-update-and-refine.zh.md +86 -0
  52. package/docs/features.md +56 -0
  53. package/docs/features.zh.md +56 -0
  54. package/docs/getting-started.md +74 -0
  55. package/docs/getting-started.zh.md +74 -0
  56. package/docs/overview.md +48 -0
  57. package/docs/overview.zh.md +48 -0
  58. package/media.md +19 -0
  59. package/package.json +13 -10
  60. package/prompts/content-detail-generator.md +7 -3
  61. package/prompts/document/custom-components.md +80 -0
  62. package/prompts/document/d2-chart/diy-examples.md +44 -0
  63. package/prompts/document/d2-chart/official-examples.md +708 -0
  64. package/prompts/document/d2-chart/rules.md +48 -0
  65. package/prompts/document/detail-generator.md +12 -15
  66. package/prompts/document/structure-planning.md +1 -3
  67. package/prompts/feedback-refiner.md +81 -60
  68. package/prompts/structure-planning.md +20 -3
  69. package/tests/check-detail-result.test.mjs +3 -4
  70. package/tests/conflict-resolution.test.mjs +237 -0
  71. package/tests/input-generator.test.mjs +940 -0
  72. package/tests/load-sources.test.mjs +627 -3
  73. package/tests/preferences-utils.test.mjs +94 -0
  74. package/tests/save-value-to-config.test.mjs +182 -5
  75. package/tests/utils.test.mjs +49 -0
  76. package/utils/conflict-detector.mjs +72 -1
  77. package/utils/constants.mjs +125 -124
  78. package/utils/kroki-utils.mjs +162 -0
  79. package/utils/markdown-checker.mjs +98 -70
  80. package/utils/utils.mjs +96 -28
@@ -187,6 +187,100 @@ describe("preferences-utils", () => {
187
187
  expect(preferences.rules[0]).toEqual(rule2); // Most recent first
188
188
  expect(preferences.rules[1]).toEqual(rule1);
189
189
  });
190
+
191
+ test("should handle rules with special characters and colons", () => {
192
+ const ruleData = {
193
+ rule: 'Always use proper punctuation: semicolons, colons, and quotes"like this"',
194
+ scope: "document",
195
+ limitToInputPaths: false,
196
+ };
197
+ const feedback = 'Please ensure proper punctuation: don\'t forget quotes"and colons"';
198
+
199
+ const newRule = addPreferenceRule(ruleData, feedback);
200
+
201
+ expect(newRule.rule).toBe(
202
+ 'Always use proper punctuation: semicolons, colons, and quotes"like this"',
203
+ );
204
+ expect(newRule.feedback).toBe(
205
+ 'Please ensure proper punctuation: don\'t forget quotes"and colons"',
206
+ );
207
+
208
+ // Verify it was saved and can be read back
209
+ const preferences = readPreferences();
210
+ expect(preferences.rules).toHaveLength(1);
211
+ expect(preferences.rules[0].rule).toEqual(newRule.rule);
212
+ expect(preferences.rules[0].feedback).toEqual(newRule.feedback);
213
+ });
214
+
215
+ test("should handle rules with multiline content and special YAML characters", () => {
216
+ const ruleData = {
217
+ rule: "Multi-line rule:\\nLine 1: Use proper formatting\\nLine 2: Handle | pipe characters\\nLine 3: And > greater than symbols",
218
+ scope: "structure",
219
+ limitToInputPaths: false,
220
+ };
221
+ const feedback =
222
+ "Multi-line feedback:\\n- Check formatting\\n- Validate | pipes\\n- Handle > symbols\\n- Process 'single quotes' and \"double quotes\"";
223
+
224
+ const newRule = addPreferenceRule(ruleData, feedback);
225
+
226
+ expect(newRule.rule).toContain("Multi-line rule:\\nLine 1");
227
+ expect(newRule.feedback).toContain("Multi-line feedback:\\n- Check");
228
+
229
+ // Verify it was saved and can be read back correctly
230
+ const preferences = readPreferences();
231
+ expect(preferences.rules).toHaveLength(1);
232
+ expect(preferences.rules[0].rule).toEqual(ruleData.rule);
233
+ expect(preferences.rules[0].feedback).toEqual(feedback);
234
+ });
235
+
236
+ test("should handle rules with Chinese and Unicode characters", () => {
237
+ const ruleData = {
238
+ rule: "使用正确的中文标点符号:逗号,句号。还有emoji 🔥 和特殊符号 @#$%^&*()",
239
+ scope: "document",
240
+ limitToInputPaths: false,
241
+ };
242
+ const feedback = '请确保正确使用标点:引号"这样"和冒号:还有各种符号!@#$%';
243
+
244
+ const newRule = addPreferenceRule(ruleData, feedback);
245
+
246
+ expect(newRule.rule).toBe(
247
+ "使用正确的中文标点符号:逗号,句号。还有emoji 🔥 和特殊符号 @#$%^&*()",
248
+ );
249
+ expect(newRule.feedback).toBe('请确保正确使用标点:引号"这样"和冒号:还有各种符号!@#$%');
250
+
251
+ // Verify it was saved and can be read back
252
+ const preferences = readPreferences();
253
+ expect(preferences.rules).toHaveLength(1);
254
+ expect(preferences.rules[0].rule).toEqual(newRule.rule);
255
+ expect(preferences.rules[0].feedback).toEqual(newRule.feedback);
256
+ });
257
+
258
+ test("should handle paths with special characters", () => {
259
+ const ruleData = {
260
+ rule: "Path-specific rule for special directories",
261
+ scope: "structure",
262
+ limitToInputPaths: true,
263
+ };
264
+ const feedback = "Apply to special paths only";
265
+ const paths = [
266
+ "/docs/api: advanced",
267
+ "/docs/guide-中文",
268
+ "/path with spaces",
269
+ "/symbols@#$%/file",
270
+ ];
271
+
272
+ const newRule = addPreferenceRule(ruleData, feedback, paths);
273
+
274
+ expect(newRule.paths).toEqual(paths);
275
+
276
+ // Verify it was saved with all special character paths
277
+ const preferences = readPreferences();
278
+ expect(preferences.rules[0].paths).toEqual(paths);
279
+ expect(preferences.rules[0].paths).toContain("/docs/api: advanced");
280
+ expect(preferences.rules[0].paths).toContain("/docs/guide-中文");
281
+ expect(preferences.rules[0].paths).toContain("/path with spaces");
282
+ expect(preferences.rules[0].paths).toContain("/symbols@#$%/file");
283
+ });
190
284
  });
191
285
 
192
286
  describe("getActiveRulesForScope", () => {
@@ -59,7 +59,7 @@ describe("saveValueToConfig", () => {
59
59
  await saveValueToConfig("projectName", "test-project");
60
60
  const content = await readConfigFile();
61
61
 
62
- expect(content).toContain('projectName: "test-project"');
62
+ expect(content).toContain("projectName: test-project");
63
63
  });
64
64
 
65
65
  test("Save string value with comment to empty file", async () => {
@@ -67,7 +67,7 @@ describe("saveValueToConfig", () => {
67
67
  const content = await readConfigFile();
68
68
 
69
69
  expect(content).toContain("# Project description");
70
- expect(content).toContain('projectDesc: "A test project"');
70
+ expect(content).toContain("projectDesc: A test project");
71
71
  });
72
72
 
73
73
  test("Save array value to empty file", async () => {
@@ -103,7 +103,7 @@ describe("saveValueToConfig", () => {
103
103
  await saveValueToConfig("projectName", "new-project");
104
104
  const content = await readConfigFile();
105
105
 
106
- expect(content).toContain('projectName: "new-project"');
106
+ expect(content).toContain("projectName: new-project");
107
107
  expect(content).toContain('version: "1.0.0"');
108
108
  expect(content).not.toContain("old-project");
109
109
  });
@@ -160,7 +160,7 @@ version: "1.0.0"`);
160
160
  const content = await readConfigFile();
161
161
 
162
162
  expect(content).toContain("# Updated project name");
163
- expect(content).toContain('projectName: "updated-project"');
163
+ expect(content).toContain("projectName: updated-project");
164
164
  });
165
165
 
166
166
  test("Update value that already has comment", async () => {
@@ -172,7 +172,7 @@ version: "1.0.0"`);
172
172
  const content = await readConfigFile();
173
173
 
174
174
  expect(content).toContain("# Project information");
175
- expect(content).toContain('projectName: "new-project"');
175
+ expect(content).toContain("projectName: new-project");
176
176
  expect(content).not.toContain("# Updated project info");
177
177
  });
178
178
 
@@ -244,4 +244,181 @@ version: "1.0.0"`);
244
244
 
245
245
  expect(content).toBe(initialContent);
246
246
  });
247
+
248
+ // Edge cases and special character tests
249
+ describe("Special characters and edge cases", () => {
250
+ test("Save string with double quotes", async () => {
251
+ await saveValueToConfig("projectDesc", 'My "awesome" project with quotes');
252
+ const content = await readConfigFile();
253
+
254
+ // Should be properly escaped
255
+ expect(content).toContain('My "awesome" project with quotes');
256
+ // Verify YAML is valid by checking structure
257
+ expect(content).toContain("projectDesc:");
258
+ });
259
+
260
+ test("Save string with single quotes", async () => {
261
+ await saveValueToConfig("projectName", "It's a 'great' project");
262
+ const content = await readConfigFile();
263
+
264
+ expect(content).toContain("It's a 'great' project");
265
+ expect(content).toContain("projectName:");
266
+ });
267
+
268
+ test("Save string with colons and special YAML characters", async () => {
269
+ await saveValueToConfig(
270
+ "rule",
271
+ "Use proper formatting: semicolons; colons: and pipes | symbols",
272
+ );
273
+ const content = await readConfigFile();
274
+
275
+ expect(content).toContain("Use proper formatting: semicolons; colons: and pipes | symbols");
276
+ expect(content).toContain("rule:");
277
+ });
278
+
279
+ test("Save multiline string with newlines", async () => {
280
+ const multilineValue =
281
+ "Line 1: First line\nLine 2: Second line\nLine 3: Third line with: special characters";
282
+ await saveValueToConfig("multilineRule", multilineValue);
283
+ const content = await readConfigFile();
284
+
285
+ expect(content).toContain("Line 1: First line");
286
+ expect(content).toContain("Line 2: Second line");
287
+ expect(content).toContain("Line 3: Third line with: special characters");
288
+ });
289
+
290
+ test("Save string with Chinese characters and symbols", async () => {
291
+ await saveValueToConfig(
292
+ "chineseProject",
293
+ '中文项目:包含特殊符号!@#¥%…()—— quotes"and" colons:',
294
+ );
295
+ const content = await readConfigFile();
296
+
297
+ expect(content).toContain('中文项目:包含特殊符号!@#¥%…()—— quotes"and" colons:');
298
+ expect(content).toContain("chineseProject:");
299
+ });
300
+
301
+ test("Save string with emoji and Unicode", async () => {
302
+ await saveValueToConfig(
303
+ "emojiProject",
304
+ "🚀 Project with emoji: 🔥 hot features 💯 and symbols ⭐ ✨ 🎉",
305
+ );
306
+ const content = await readConfigFile();
307
+
308
+ expect(content).toContain("🚀 Project with emoji: 🔥 hot features 💯 and symbols ⭐ ✨ 🎉");
309
+ expect(content).toContain("emojiProject:");
310
+ });
311
+
312
+ test("Save array with special character items", async () => {
313
+ const specialPaths = [
314
+ "./src: main source",
315
+ "./lib/utils: helper functions",
316
+ "./docs-中文/guide",
317
+ "./path with spaces/file",
318
+ "./symbols@#$%/directory",
319
+ './quotes"and"colons:/path',
320
+ ];
321
+ await saveValueToConfig("specialPaths", specialPaths);
322
+ const content = await readConfigFile();
323
+
324
+ expect(content).toContain("specialPaths:");
325
+ specialPaths.forEach((path) => {
326
+ expect(content).toContain(path);
327
+ });
328
+ });
329
+
330
+ test("Save array with multiline items", async () => {
331
+ const multilineArray = [
332
+ "Item 1:\nWith newline content",
333
+ "Item 2: Simple item",
334
+ "Item 3:\nMultiple\nLines\nWith: colons and | pipes",
335
+ ];
336
+ await saveValueToConfig("multilineArray", multilineArray);
337
+ const content = await readConfigFile();
338
+
339
+ expect(content).toContain("multilineArray:");
340
+ expect(content).toContain("Item 1:");
341
+ expect(content).toContain("With newline content");
342
+ expect(content).toContain("Item 3:");
343
+ expect(content).toContain("With: colons and | pipes");
344
+ });
345
+
346
+ test("Update existing value containing special characters", async () => {
347
+ await createInitialConfig('projectName: "old: project with | pipes"\nversion: "1.0.0"');
348
+
349
+ await saveValueToConfig("projectName", 'new: project with "quotes" and | pipes: updated');
350
+ const content = await readConfigFile();
351
+
352
+ expect(content).toContain('new: project with "quotes" and | pipes: updated');
353
+ expect(content).toContain('version: "1.0.0"');
354
+ expect(content).not.toContain("old: project");
355
+ });
356
+
357
+ test("Save very long string with special characters", async () => {
358
+ const longString =
359
+ "A very long project description that contains many special characters: colons, semicolons; quotes \"like this\", single quotes 'like this', pipes | symbols, Chinese characters 中文内容:包含各种符号!@#¥%…()——, emojis 🚀🔥💯⭐✨🎉, and newlines\nSecond line with more content\nThird line: even more special content with all sorts of symbols @#$%^&*()_+-=[]{}|\\;':./?,<>~`";
360
+ await saveValueToConfig("veryLongDesc", longString);
361
+ const content = await readConfigFile();
362
+
363
+ expect(content).toContain("veryLongDesc:");
364
+ expect(content).toContain("A very long project description");
365
+ expect(content).toContain("中文内容:包含各种符号");
366
+ expect(content).toContain("🚀🔥💯⭐✨🎉");
367
+ expect(content).toContain("Second line with more content");
368
+ });
369
+
370
+ test("Save empty string", async () => {
371
+ await saveValueToConfig("emptyString", "");
372
+ const content = await readConfigFile();
373
+
374
+ expect(content).toContain('emptyString: ""');
375
+ });
376
+
377
+ test("Save string that looks like YAML syntax", async () => {
378
+ await saveValueToConfig("yamlLikeString", "key: value\nother: - item1\n- item2: subvalue");
379
+ const content = await readConfigFile();
380
+
381
+ expect(content).toContain("yamlLikeString:");
382
+ expect(content).toContain("key: value");
383
+ expect(content).toContain("other: - item1");
384
+ expect(content).toContain("- item2: subvalue");
385
+ });
386
+
387
+ test("Complex mixed scenario with multiple special character values", async () => {
388
+ // Create initial config with some special content
389
+ await createInitialConfig(`# Project configuration
390
+ # with special comments: and symbols
391
+ projectName: "original: project"
392
+ locale: zh`);
393
+
394
+ // Update with various special character content
395
+ await saveValueToConfig("projectDesc", 'Updated description: with "quotes" and | pipes');
396
+ await saveValueToConfig("specialArray", [
397
+ "path: with colons",
398
+ "中文路径/文件",
399
+ "emoji 🔥 path",
400
+ ]);
401
+ await saveValueToConfig(
402
+ "multilineContent",
403
+ "Line 1: content\nLine 2: more content\nLine 3: final content",
404
+ );
405
+
406
+ const content = await readConfigFile();
407
+
408
+ // Check all content is preserved
409
+ expect(content).toContain("# Project configuration");
410
+ expect(content).toContain("# with special comments: and symbols");
411
+ expect(content).toContain('projectName: "original: project"');
412
+ expect(content).toContain("locale: zh");
413
+ expect(content).toContain('Updated description: with "quotes" and | pipes');
414
+ expect(content).toContain("specialArray:");
415
+ expect(content).toContain("path: with colons");
416
+ expect(content).toContain("中文路径/文件");
417
+ expect(content).toContain("emoji 🔥 path");
418
+ expect(content).toContain("multilineContent:");
419
+ expect(content).toContain("Line 1: content");
420
+ expect(content).toContain("Line 2: more content");
421
+ expect(content).toContain("Line 3: final content");
422
+ });
423
+ });
247
424
  });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { isGlobPattern } from "../utils/utils.mjs";
3
+
4
+ describe("utils", () => {
5
+ describe("isGlobPattern", () => {
6
+ test("should return true for patterns with asterisk", () => {
7
+ expect(isGlobPattern("*.js")).toBe(true);
8
+ expect(isGlobPattern("src/*.js")).toBe(true);
9
+ expect(isGlobPattern("**/*.js")).toBe(true);
10
+ expect(isGlobPattern("src/**/*.js")).toBe(true);
11
+ });
12
+
13
+ test("should return true for patterns with question mark", () => {
14
+ expect(isGlobPattern("file?.js")).toBe(true);
15
+ expect(isGlobPattern("src/?odal.js")).toBe(true);
16
+ });
17
+
18
+ test("should return true for patterns with character classes", () => {
19
+ expect(isGlobPattern("file[abc].js")).toBe(true);
20
+ expect(isGlobPattern("src/[Bb]utton.js")).toBe(true);
21
+ expect(isGlobPattern("file[0-9].js")).toBe(true);
22
+ });
23
+
24
+ test("should return true for patterns with double asterisk", () => {
25
+ expect(isGlobPattern("**/file.js")).toBe(true);
26
+ expect(isGlobPattern("src/**/file.js")).toBe(true);
27
+ });
28
+
29
+ test("should return false for regular paths", () => {
30
+ expect(isGlobPattern("src/file.js")).toBe(false);
31
+ expect(isGlobPattern("./src")).toBe(false);
32
+ expect(isGlobPattern("/absolute/path")).toBe(false);
33
+ expect(isGlobPattern("regular-file.js")).toBe(false);
34
+ expect(isGlobPattern("package.json")).toBe(false);
35
+ });
36
+
37
+ test("should return false for empty or undefined input", () => {
38
+ expect(isGlobPattern("")).toBe(false);
39
+ expect(isGlobPattern(undefined)).toBe(false);
40
+ expect(isGlobPattern(null)).toBe(false);
41
+ });
42
+
43
+ test("should handle complex patterns", () => {
44
+ expect(isGlobPattern("src/**/*.{js,ts,jsx,tsx}")).toBe(true);
45
+ expect(isGlobPattern("components/**/[A-Z]*.js")).toBe(true);
46
+ expect(isGlobPattern("test/**/*test.js")).toBe(true);
47
+ });
48
+ });
49
+ });
@@ -1,4 +1,4 @@
1
- import { CONFLICT_RULES } from "./constants.mjs";
1
+ import { CONFLICT_RESOLUTION_RULES, CONFLICT_RULES, RESOLUTION_STRATEGIES } from "./constants.mjs";
2
2
 
3
3
  /**
4
4
  * Detect internal conflicts within the same question (multi-select conflicts)
@@ -129,3 +129,74 @@ export function validateSelection(questionType, selectedValues) {
129
129
  // For moderate conflicts, allow but warn
130
130
  return true;
131
131
  }
132
+
133
+ /**
134
+ * Detect conflicts in user configuration selections that can be resolved through structure planning
135
+ * @param {Object} config - User configuration
136
+ * @returns {Array} Array of detected conflicts with resolution strategies
137
+ */
138
+ export function detectResolvableConflicts(config) {
139
+ const conflicts = [];
140
+
141
+ // Check each question type for conflicts
142
+ Object.entries(CONFLICT_RESOLUTION_RULES).forEach(([questionType, rules]) => {
143
+ const selectedValues = config[questionType];
144
+
145
+ if (!selectedValues || !Array.isArray(selectedValues) || selectedValues.length < 2) {
146
+ return; // Skip if not multi-select or less than 2 items
147
+ }
148
+
149
+ // Extract values from the selected items (handle both string arrays and object arrays)
150
+ const selectedValueStrings = selectedValues.map((item) =>
151
+ typeof item === "object" && item.value ? item.value : item,
152
+ );
153
+
154
+ // Check each conflict rule
155
+ rules.forEach((rule) => {
156
+ // Check if all conflict items are selected
157
+ const hasConflict = rule.conflictItems.every((item) => selectedValueStrings.includes(item));
158
+
159
+ if (hasConflict) {
160
+ conflicts.push({
161
+ type: questionType,
162
+ items: rule.conflictItems,
163
+ strategy: rule.strategy,
164
+ description: rule.description,
165
+ });
166
+ }
167
+ });
168
+ });
169
+
170
+ return conflicts;
171
+ }
172
+
173
+ /**
174
+ * Generate conflict resolution rules based on detected conflicts
175
+ * @param {Array} conflicts - Array of detected conflicts
176
+ * @returns {string} Conflict resolution instructions
177
+ */
178
+ export function generateConflictResolutionRules(conflicts) {
179
+ if (conflicts.length === 0) return "";
180
+
181
+ const rules = ["=== Conflict Resolution Guidelines ==="];
182
+
183
+ conflicts.forEach((conflict) => {
184
+ const strategy = RESOLUTION_STRATEGIES[conflict.strategy];
185
+ if (strategy) {
186
+ rules.push(strategy(conflict.items));
187
+ }
188
+ });
189
+
190
+ rules.push("");
191
+ rules.push("Conflict Resolution Principles:");
192
+ rules.push(
193
+ "1. Meet diverse needs through intelligent structural design, not simple concatenation",
194
+ );
195
+ rules.push("2. Create clear navigation paths for different purposes and audiences");
196
+ rules.push(
197
+ "3. Ensure content hierarchy is reasonable, avoid information duplication or contradiction",
198
+ );
199
+ rules.push("4. Prioritize user experience, enable users to quickly find needed information");
200
+
201
+ return rules.join("\n");
202
+ }