@aigne/doc-smith 0.8.11 → 0.8.12-beta.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 (56) hide show
  1. package/.github/workflows/publish-docs.yml +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +19 -0
  4. package/agents/generate/check-diagram.mjs +40 -0
  5. package/agents/generate/draw-diagram.yaml +23 -0
  6. package/agents/generate/generate-structure.yaml +5 -1
  7. package/agents/generate/merge-d2-diagram.yaml +3 -3
  8. package/agents/generate/update-document-structure.yaml +5 -2
  9. package/agents/generate/user-review-document-structure.mjs +7 -0
  10. package/agents/generate/wrap-diagram-code.mjs +35 -0
  11. package/agents/history/index.yaml +6 -0
  12. package/agents/history/view.mjs +75 -0
  13. package/agents/translate/index.yaml +3 -2
  14. package/agents/translate/record-translation-history.mjs +19 -0
  15. package/agents/translate/translate-multilingual.yaml +2 -1
  16. package/agents/update/batch-update-document.yaml +1 -1
  17. package/agents/update/check-document.mjs +1 -1
  18. package/agents/update/fs-tools/read-file.mjs +3 -1
  19. package/agents/update/generate-document.yaml +31 -25
  20. package/agents/update/{generate-and-translate-document.yaml → handle-document-update.yaml} +2 -11
  21. package/agents/update/index.yaml +1 -0
  22. package/agents/update/save-and-translate-document.mjs +101 -0
  23. package/agents/update/update-document-detail.yaml +5 -1
  24. package/agents/update/update-single-document.yaml +1 -10
  25. package/agents/update/user-review-document.mjs +4 -1
  26. package/aigne.yaml +8 -1
  27. package/package.json +1 -1
  28. package/prompts/detail/d2-diagram/guide.md +19 -0
  29. package/prompts/detail/d2-diagram/role-and-personality.md +2 -0
  30. package/prompts/detail/d2-diagram/rules.md +24 -0
  31. package/prompts/detail/d2-diagram/{rules-system.md → system-prompt.md} +3 -9
  32. package/prompts/detail/{document-rules.md → generate/document-rules.md} +1 -1
  33. package/prompts/detail/generate/system-prompt.md +72 -0
  34. package/prompts/detail/generate/user-prompt.md +54 -0
  35. package/prompts/detail/{update-document.md → update/system-prompt.md} +43 -67
  36. package/prompts/detail/update/user-prompt.md +33 -0
  37. package/prompts/structure/{generate-structure-system.md → generate/system-prompt.md} +7 -40
  38. package/prompts/structure/{generate-structure-user.md → generate/user-prompt.md} +17 -13
  39. package/prompts/structure/{update-document-structure.md → update/system-prompt.md} +16 -27
  40. package/prompts/structure/update/user-prompt.md +19 -0
  41. package/tests/agents/generate/user-review-document-structure.test.mjs +2 -0
  42. package/tests/agents/update/check-document.test.mjs +1 -1
  43. package/tests/agents/update/fs-tools/read-file.test.mjs +8 -12
  44. package/tests/agents/utils/check-detail-result.test.mjs +13 -0
  45. package/tests/utils/d2-utils.test.mjs +14 -0
  46. package/tests/utils/docs-finder-utils.test.mjs +13 -0
  47. package/tests/utils/history-utils.test.mjs +178 -0
  48. package/utils/d2-utils.mjs +9 -0
  49. package/utils/docs-finder-utils.mjs +10 -1
  50. package/utils/history-utils.mjs +191 -0
  51. package/utils/markdown-checker.mjs +20 -0
  52. package/agents/generate/check-d2-diagram-valid.mjs +0 -26
  53. package/agents/generate/generate-d2-diagram.yaml +0 -23
  54. package/prompts/detail/generate-document.md +0 -125
  55. /package/prompts/detail/d2-diagram/{rules-user.md → user-prompt.md} +0 -0
  56. /package/prompts/detail/{detail-example.md → generate/detail-example.md} +0 -0
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:
2
2
  import userReviewDocumentStructure from "../../../agents/generate/user-review-document-structure.mjs";
3
3
 
4
4
  import * as preferencesUtils from "../../../utils/preferences-utils.mjs";
5
+ import * as historyUtils from "../../../utils/history-utils.mjs";
5
6
 
6
7
  describe("user-review-document-structure", () => {
7
8
  let mockOptions;
@@ -61,6 +62,7 @@ describe("user-review-document-structure", () => {
61
62
  );
62
63
 
63
64
  consoleSpy = spyOn(console, "log").mockImplementation(() => {});
65
+ spyOn(historyUtils, "recordUpdate").mockImplementation(() => {});
64
66
 
65
67
  // Clear prompts mock call history
66
68
  mockOptions.prompts.select.mockClear();
@@ -25,7 +25,7 @@ describe("check-document", () => {
25
25
  mockOptions = {
26
26
  context: {
27
27
  agents: {
28
- generateAndTranslateDocument: { mockAgent: true },
28
+ handleDocumentUpdate: { mockAgent: true },
29
29
  },
30
30
  invoke: mock(async () => ({ mockResult: true })),
31
31
  },
@@ -1,8 +1,8 @@
1
+ import { afterEach, beforeEach, describe, it } from "bun:test";
1
2
  import assert from "node:assert";
2
3
  import fs from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
- import { afterEach, beforeEach, describe, it } from "bun:test";
6
6
  import readFile from "../../../../agents/update/fs-tools/read-file.mjs";
7
7
 
8
8
  describe("read-file tool", () => {
@@ -11,11 +11,13 @@ describe("read-file tool", () => {
11
11
  beforeEach(async () => {
12
12
  // Create temporary directory for test files
13
13
  tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "read-file-test-"));
14
+ process.chdir(tempDir);
14
15
  });
15
16
 
16
17
  afterEach(async () => {
17
18
  // Clean up temporary directory
18
19
  await fs.rm(tempDir, { recursive: true, force: true });
20
+ process.chdir(__dirname);
19
21
  });
20
22
 
21
23
  describe("basic functionality", () => {
@@ -25,13 +27,16 @@ describe("read-file tool", () => {
25
27
  await fs.writeFile(filePath, content, "utf8");
26
28
 
27
29
  const result = await readFile({
28
- path: filePath,
30
+ path: "test.txt",
29
31
  });
30
32
 
31
33
  assert.strictEqual(result.command, "read_file");
32
34
  assert.strictEqual(result.error, null);
33
35
  assert.strictEqual(result.result.content, content);
34
- assert.strictEqual(result.result.metadata.path, filePath);
36
+ assert.strictEqual(
37
+ await fs.realpath(result.result.metadata.path),
38
+ await fs.realpath(filePath),
39
+ );
35
40
  assert.strictEqual(result.result.metadata.mimeType, "text/plain");
36
41
  assert.strictEqual(result.result.metadata.isBinary, false);
37
42
  assert.strictEqual(result.result.metadata.encoding, "utf8");
@@ -348,15 +353,6 @@ describe("read-file tool", () => {
348
353
  assert(result.error.message.includes("required"));
349
354
  });
350
355
 
351
- it("should require absolute path", async () => {
352
- const result = await readFile({
353
- path: "relative/path.txt",
354
- });
355
-
356
- assert.notStrictEqual(result.error, null);
357
- assert(result.error.message.includes("absolute"));
358
- });
359
-
360
356
  it("should handle non-existent files", async () => {
361
357
  const result = await readFile({
362
358
  path: path.join(tempDir, "nonexistent.txt"),
@@ -337,6 +337,19 @@ This document demonstrates various programming language code blocks.`;
337
337
  });
338
338
  });
339
339
 
340
+ describe("D2 syntax validation", () => {
341
+ test("should handle D2 syntax errors", async () => {
342
+ const documentStructure = [];
343
+ const reviewContent =
344
+ "```d2\n" +
345
+ "invalid d2 syntax {{\n" + // Malformed D2
346
+ "```\n\n" +
347
+ "This has proper structure.";
348
+ const result = await checkDetailResult({ documentStructure, reviewContent });
349
+ expect(result.isApproved).toBe(false);
350
+ });
351
+ });
352
+
340
353
  describe("Advanced table edge cases", () => {
341
354
  test("should handle empty table cells correctly", async () => {
342
355
  const documentStructure = [];
@@ -14,6 +14,7 @@ import {
14
14
  getChart,
15
15
  isValidCode,
16
16
  saveAssets,
17
+ wrapCode,
17
18
  } from "../../utils/d2-utils.mjs";
18
19
 
19
20
  describe("d2-utils", () => {
@@ -420,4 +421,17 @@ E -> F
420
421
  expect(isValidCode(undefined)).toBe(false);
421
422
  });
422
423
  });
424
+
425
+ describe("wrapCode", () => {
426
+ test("should return original content when D2 block already exists", () => {
427
+ const content = "```d2\nA -> B\n```";
428
+ expect(wrapCode({ content })).toBe(content);
429
+ });
430
+
431
+ test("should wrap plain content in a D2 code block", () => {
432
+ const content = "A -> B";
433
+ const expected = "```d2\nA -> B\n```";
434
+ expect(wrapCode({ content })).toBe(expected);
435
+ });
436
+ });
423
437
  });
@@ -6,6 +6,7 @@ import {
6
6
  fileNameToFlatPath,
7
7
  findItemByFlatName,
8
8
  findItemByPath,
9
+ generateFileName,
9
10
  getActionText,
10
11
  getMainLanguageFiles,
11
12
  processSelectedFiles,
@@ -633,5 +634,17 @@ describe("docs-finder-utils", () => {
633
634
  "⚠️ No documentation structure item found for file: test.md",
634
635
  );
635
636
  });
637
+
638
+ test("fileNameToFlatPath should handle files with multiple language suffixes", () => {
639
+ expect(fileNameToFlatPath("file.zh-CN.md")).toBe("file");
640
+ expect(fileNameToFlatPath("file.en-US.md")).toBe("file");
641
+ expect(fileNameToFlatPath("file.fr-FR.md")).toBe("file");
642
+ });
643
+
644
+ test("generateFileName should handle special characters in flatName", () => {
645
+ expect(generateFileName("api-v1-guide", "en")).toBe("api-v1-guide.md");
646
+ expect(generateFileName("api-v1-guide", "zh")).toBe("api-v1-guide.zh.md");
647
+ expect(generateFileName("test_special-chars", "fr")).toBe("test_special-chars.fr.md");
648
+ });
636
649
  });
637
650
  });
@@ -0,0 +1,178 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { DOC_SMITH_DIR } from "../../utils/constants/index.mjs";
5
+ import { getHistory, isGitAvailable, recordUpdate } from "../../utils/history-utils.mjs";
6
+
7
+ const TEST_DIR = join(process.cwd(), `${DOC_SMITH_DIR}-test`);
8
+ const ORIGINAL_CWD = process.cwd();
9
+
10
+ describe("History Utils - Unified", () => {
11
+ beforeEach(() => {
12
+ // Clean up test directory
13
+ if (existsSync(TEST_DIR)) {
14
+ rmSync(TEST_DIR, { recursive: true });
15
+ }
16
+ mkdirSync(TEST_DIR, { recursive: true });
17
+
18
+ // Change to test directory
19
+ process.chdir(TEST_DIR);
20
+ });
21
+
22
+ afterEach(() => {
23
+ // Restore original directory
24
+ process.chdir(ORIGINAL_CWD);
25
+
26
+ // Clean up
27
+ if (existsSync(TEST_DIR)) {
28
+ rmSync(TEST_DIR, { recursive: true });
29
+ }
30
+ });
31
+
32
+ test("detects git availability", () => {
33
+ const hasGit = isGitAvailable();
34
+ expect(typeof hasGit).toBe("boolean");
35
+ });
36
+
37
+ test("skips recording on empty feedback", () => {
38
+ recordUpdate({ operation: "document_update", feedback: "" });
39
+ const history = getHistory();
40
+ expect(history.entries.length).toBe(0);
41
+ });
42
+
43
+ test("skips recording on whitespace-only feedback", () => {
44
+ recordUpdate({ operation: "document_update", feedback: " " });
45
+ const history = getHistory();
46
+ expect(history.entries.length).toBe(0);
47
+ });
48
+
49
+ test("records update in YAML", () => {
50
+ recordUpdate({
51
+ operation: "structure_update",
52
+ feedback: "Test feedback",
53
+ });
54
+
55
+ const history = getHistory();
56
+ expect(history.entries.length).toBe(1);
57
+ expect(history.entries[0].feedback).toBe("Test feedback");
58
+ expect(history.entries[0].operation).toBe("structure_update");
59
+ expect(history.entries[0].timestamp).toBeDefined();
60
+ });
61
+
62
+ test("records document path when provided as string", () => {
63
+ recordUpdate({
64
+ operation: "document_update",
65
+ feedback: "Update document",
66
+ documentPath: "/getting-started",
67
+ });
68
+
69
+ const history = getHistory();
70
+ expect(history.entries.length).toBe(1);
71
+ expect(history.entries[0].documentPath).toBe("/getting-started");
72
+ });
73
+
74
+ test("records single document path for each update", () => {
75
+ recordUpdate({
76
+ operation: "document_update",
77
+ feedback: "Update single document",
78
+ documentPath: "/getting-started",
79
+ });
80
+
81
+ const history = getHistory();
82
+ expect(history.entries.length).toBe(1);
83
+ expect(history.entries[0].feedback).toBe("Update single document");
84
+ expect(history.entries[0].documentPath).toBe("/getting-started");
85
+ });
86
+
87
+ test("does not include documentPath field when documentPath is null", () => {
88
+ recordUpdate({
89
+ operation: "structure_update",
90
+ feedback: "Update structure",
91
+ documentPath: null,
92
+ });
93
+
94
+ const history = getHistory();
95
+ expect(history.entries.length).toBe(1);
96
+ expect(history.entries[0].documentPath).toBeUndefined();
97
+ });
98
+
99
+ test("maintains chronological order (newest first)", () => {
100
+ recordUpdate({ operation: "structure_update", feedback: "First" });
101
+ // Small delay to ensure different timestamps
102
+ const now = Date.now();
103
+ while (Date.now() === now) {
104
+ // Wait for next millisecond
105
+ }
106
+ recordUpdate({ operation: "document_update", feedback: "Second" });
107
+
108
+ const history = getHistory();
109
+ expect(history.entries.length).toBe(2);
110
+ expect(history.entries[0].feedback).toBe("Second");
111
+ expect(history.entries[1].feedback).toBe("First");
112
+ });
113
+
114
+ test("handles multiple updates", () => {
115
+ recordUpdate({ operation: "structure_update", feedback: "Update 1" });
116
+ recordUpdate({ operation: "document_update", feedback: "Update 2", documentPath: "/home" });
117
+ recordUpdate({ operation: "document_update", feedback: "Update 3", documentPath: "/about" });
118
+ recordUpdate({
119
+ operation: "translation_update",
120
+ feedback: "Update 4",
121
+ documentPath: "/api",
122
+ });
123
+
124
+ const history = getHistory();
125
+ expect(history.entries.length).toBe(4);
126
+ expect(history.entries[0].feedback).toBe("Update 4");
127
+ expect(history.entries[0].documentPath).toBe("/api");
128
+ expect(history.entries[1].feedback).toBe("Update 3");
129
+ expect(history.entries[1].documentPath).toBe("/about");
130
+ expect(history.entries[2].feedback).toBe("Update 2");
131
+ expect(history.entries[2].documentPath).toBe("/home");
132
+ expect(history.entries[3].feedback).toBe("Update 1");
133
+ });
134
+
135
+ test("returns empty history when file does not exist", () => {
136
+ const history = getHistory();
137
+ expect(history.entries).toBeDefined();
138
+ expect(history.entries.length).toBe(0);
139
+ });
140
+
141
+ test("handles corrupted history file gracefully", () => {
142
+ // Create corrupted YAML file
143
+ const historyPath = join(process.cwd(), DOC_SMITH_DIR, "history.yaml");
144
+ mkdirSync(join(process.cwd(), DOC_SMITH_DIR), { recursive: true });
145
+ writeFileSync(historyPath, "invalid: yaml: content: [[[", "utf8");
146
+
147
+ const history = getHistory();
148
+ expect(history.entries).toBeDefined();
149
+ expect(history.entries.length).toBe(0);
150
+ });
151
+
152
+ test(`creates ${DOC_SMITH_DIR} directory if not exists`, () => {
153
+ recordUpdate({
154
+ operation: "document_update",
155
+ feedback: "Test",
156
+ });
157
+
158
+ const docSmithDir = join(process.cwd(), DOC_SMITH_DIR);
159
+ expect(existsSync(docSmithDir)).toBe(true);
160
+ });
161
+
162
+ test("timestamp is in ISO 8601 format", () => {
163
+ recordUpdate({
164
+ operation: "structure_update",
165
+ feedback: "Test timestamp",
166
+ });
167
+
168
+ const history = getHistory();
169
+ const timestamp = history.entries[0].timestamp;
170
+
171
+ // Validate ISO 8601 format
172
+ expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
173
+
174
+ // Validate it's a valid date
175
+ const date = new Date(timestamp);
176
+ expect(date.toISOString()).toBe(timestamp);
177
+ });
178
+ });
@@ -195,3 +195,12 @@ export async function ensureTmpDir() {
195
195
  export function isValidCode(lang) {
196
196
  return lang?.toLowerCase() === "d2";
197
197
  }
198
+
199
+ export function wrapCode({ content }) {
200
+ const matches = Array.from(content.matchAll(codeBlockRegex));
201
+ if (matches.length > 0) {
202
+ return content;
203
+ }
204
+
205
+ return `\`\`\`d2\n${content}\n\`\`\``;
206
+ }
@@ -12,13 +12,22 @@ export function getActionText(isTranslate, baseText) {
12
12
  return baseText.replace("{action}", action);
13
13
  }
14
14
 
15
+ /**
16
+ * Convert path to flattened name format
17
+ * @param {string} path - Document path (e.g., "/api/users")
18
+ * @returns {string} Flattened name (e.g., "api-users")
19
+ */
20
+ export function pathToFlatName(path) {
21
+ return path.replace(/^\//, "").replace(/\//g, "-");
22
+ }
23
+
15
24
  /**
16
25
  * Generate filename based on flattened path and locale
17
26
  * @param {string} flatName - Flattened path name
18
27
  * @param {string} locale - Main language locale (e.g., 'en', 'zh', 'fr')
19
28
  * @returns {string} Generated filename
20
29
  */
21
- function generateFileName(flatName, locale) {
30
+ export function generateFileName(flatName, locale) {
22
31
  const isEnglish = locale === "en";
23
32
  return isEnglish ? `${flatName}.md` : `${flatName}.${locale}.md`;
24
33
  }
@@ -0,0 +1,191 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { parse, stringify } from "yaml";
5
+ import { DOC_SMITH_DIR } from "./constants/index.mjs";
6
+
7
+ const HISTORY_FILE = "history.yaml";
8
+
9
+ /**
10
+ * Check if git is available in the system
11
+ */
12
+ export function isGitAvailable() {
13
+ try {
14
+ execSync("git --version", { stdio: "ignore" });
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Initialize git repo in DOC_SMITH_DIR if not exists
23
+ */
24
+ export function ensureGitRepo() {
25
+ if (!isGitAvailable()) return false;
26
+
27
+ const gitDir = join(process.cwd(), DOC_SMITH_DIR, ".git");
28
+
29
+ if (!existsSync(gitDir)) {
30
+ try {
31
+ const cwd = join(process.cwd(), DOC_SMITH_DIR);
32
+
33
+ execSync("git init", { cwd, stdio: "ignore" });
34
+
35
+ // Create .gitignore to exclude temporary files
36
+ const gitignore = "*.tmp\n";
37
+ writeFileSync(join(cwd, ".gitignore"), gitignore);
38
+
39
+ // Initial commit
40
+ execSync('git add .gitignore && git commit -m "Initialize doc-smith history"', {
41
+ cwd,
42
+ stdio: "ignore",
43
+ });
44
+
45
+ console.log("✔ Git history tracking initialized");
46
+ return true;
47
+ } catch (error) {
48
+ console.warn("Failed to initialize git history:", error.message);
49
+ return false;
50
+ }
51
+ }
52
+
53
+ return true;
54
+ }
55
+
56
+ /**
57
+ * Record update using git commit (if available)
58
+ */
59
+ function recordUpdateGit({ feedback }) {
60
+ try {
61
+ const cwd = join(process.cwd(), DOC_SMITH_DIR);
62
+
63
+ // Stage changed files (only if they exist)
64
+ const filesToAdd = ["docs/", "config.yaml", "preferences.yml", "history.yaml"]
65
+ .filter((file) => existsSync(join(cwd, file)))
66
+ .join(" ");
67
+
68
+ if (filesToAdd) {
69
+ execSync(`git add ${filesToAdd}`, {
70
+ cwd,
71
+ stdio: "ignore",
72
+ });
73
+ }
74
+
75
+ // Check if there are changes to commit
76
+ try {
77
+ execSync("git diff --cached --quiet", { cwd, stdio: "ignore" });
78
+ console.log("✔ No update history changes to commit");
79
+ return; // No changes
80
+ } catch {
81
+ // Has changes, continue
82
+ }
83
+
84
+ // Build commit message (only user feedback)
85
+ const message = feedback;
86
+
87
+ // Commit
88
+ execSync(`git commit -m ${JSON.stringify(message)}`, {
89
+ cwd,
90
+ stdio: "ignore",
91
+ });
92
+ console.log("✔ Update history committed successfully");
93
+ } catch (error) {
94
+ console.warn("Update history commit failed:", error.message);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Record update in YAML file (always)
100
+ */
101
+ function recordUpdateYaml({ operation, feedback, documentPath = null }) {
102
+ try {
103
+ const docSmithDir = join(process.cwd(), DOC_SMITH_DIR);
104
+ if (!existsSync(docSmithDir)) {
105
+ mkdirSync(docSmithDir, { recursive: true });
106
+ }
107
+ const historyPath = join(docSmithDir, HISTORY_FILE);
108
+
109
+ // Read existing history
110
+ let history = { entries: [] };
111
+ if (existsSync(historyPath)) {
112
+ try {
113
+ const content = readFileSync(historyPath, "utf8");
114
+ history = parse(content) || { entries: [] };
115
+ } catch (error) {
116
+ console.warn("Failed to read history file:", error.message);
117
+ }
118
+ }
119
+
120
+ // Create new entry
121
+ const entry = {
122
+ timestamp: new Date().toISOString(),
123
+ operation,
124
+ feedback,
125
+ };
126
+
127
+ // Add document path if provided
128
+ if (documentPath) {
129
+ entry.documentPath = documentPath;
130
+ }
131
+
132
+ // Add to beginning (newest first)
133
+ history.entries = history.entries || [];
134
+ history.entries.unshift(entry);
135
+
136
+ // Write back
137
+ const yamlContent = stringify(history, {
138
+ indent: 2,
139
+ lineWidth: 100,
140
+ });
141
+
142
+ writeFileSync(historyPath, yamlContent, "utf8");
143
+ } catch (error) {
144
+ console.warn("YAML history tracking failed:", error.message);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Record an update after user feedback
150
+ * - Always writes to YAML
151
+ * - Also commits to git if available
152
+ * @param {Object} params
153
+ * @param {string} params.operation - Type of operation (e.g., 'document_update', 'structure_update', 'translation_update')
154
+ * @param {string} params.feedback - User feedback text
155
+ * @param {string} params.documentPath - Document path - should be a string
156
+ */
157
+ export function recordUpdate({ operation, feedback, documentPath = null }) {
158
+ // Skip if no feedback
159
+ if (!feedback?.trim()) return;
160
+
161
+ // Always record in YAML
162
+ recordUpdateYaml({ operation, feedback, documentPath });
163
+
164
+ // Also record in git if available
165
+ if (isGitAvailable()) {
166
+ // Initialize git repo on first update if not exists
167
+ ensureGitRepo();
168
+ recordUpdateGit({ feedback });
169
+ } else {
170
+ console.warn("Git is not available, skipping git based update history");
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Get history entries from YAML
176
+ */
177
+ export function getHistory() {
178
+ const historyPath = join(process.cwd(), DOC_SMITH_DIR, HISTORY_FILE);
179
+
180
+ if (!existsSync(historyPath)) {
181
+ return { entries: [] };
182
+ }
183
+
184
+ try {
185
+ const content = readFileSync(historyPath, "utf8");
186
+ return parse(content) || { entries: [] };
187
+ } catch (error) {
188
+ console.warn("Failed to read history:", error.message);
189
+ return { entries: [] };
190
+ }
191
+ }
@@ -1,11 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import pMap from "p-map";
3
4
  import remarkGfm from "remark-gfm";
4
5
  import remarkLint from "remark-lint";
5
6
  import remarkParse from "remark-parse";
6
7
  import { unified } from "unified";
7
8
  import { visit } from "unist-util-visit";
8
9
  import { VFile } from "vfile";
10
+ import { KROKI_CONCURRENCY } from "./constants/index.mjs";
11
+ import { checkContent, isValidCode } from "./d2-utils.mjs";
9
12
  import { validateMermaidSyntax } from "./mermaid-validator.mjs";
10
13
 
11
14
  /**
@@ -375,6 +378,7 @@ export async function checkMarkdown(markdown, source = "content", options = {})
375
378
 
376
379
  // Check mermaid code blocks and other custom validations
377
380
  const mermaidChecks = [];
381
+ const d2ChecksList = [];
378
382
  visit(ast, "code", (node) => {
379
383
  if (node.lang) {
380
384
  const line = node.position?.start?.line || "unknown";
@@ -463,6 +467,12 @@ export async function checkMarkdown(markdown, source = "content", options = {})
463
467
  specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
464
468
  }
465
469
  }
470
+ if (isValidCode(node.lang)) {
471
+ d2ChecksList.push({
472
+ content: node.value,
473
+ line,
474
+ });
475
+ }
466
476
  }
467
477
  });
468
478
 
@@ -514,6 +524,16 @@ export async function checkMarkdown(markdown, source = "content", options = {})
514
524
  // Wait for all mermaid checks to complete
515
525
  await Promise.all(mermaidChecks);
516
526
 
527
+ await pMap(
528
+ d2ChecksList,
529
+ async ({ content, line }) =>
530
+ checkContent({ content }).catch((err) => {
531
+ const errorMessage = err?.message || String(err) || "Unknown d2 syntax error";
532
+ errorMessages.push(`Found D2 syntax error in ${source} at line ${line}: ${errorMessage}`);
533
+ }),
534
+ { concurrency: KROKI_CONCURRENCY },
535
+ );
536
+
517
537
  // Run markdown linting rules
518
538
  await processor.run(ast, file);
519
539
 
@@ -1,26 +0,0 @@
1
- import { checkContent } from "../../utils/d2-utils.mjs";
2
-
3
- export default async function checkD2DiagramIsValid({ d2DiagramSourceCode }) {
4
- try {
5
- await checkContent({ content: d2DiagramSourceCode });
6
- return {
7
- isValid: true,
8
- };
9
- } catch (err) {
10
- return {
11
- isValid: false,
12
- error: err.message,
13
- };
14
- }
15
- }
16
-
17
- checkD2DiagramIsValid.input_schema = {
18
- type: "object",
19
- properties: {
20
- d2DiagramSourceCode: {
21
- type: "string",
22
- description: "Source code of d2 diagram",
23
- },
24
- },
25
- required: ["d2DiagramSourceCode"],
26
- };
@@ -1,23 +0,0 @@
1
- name: drawD2Diagram
2
- description: Generate a D2 diagram from document content.
3
- instructions:
4
- - role: system
5
- url: ../../prompts/detail/d2-diagram/rules-system.md
6
- - role: user
7
- url: ../../prompts/detail/d2-diagram/rules-user.md
8
- input_schema:
9
- type: object
10
- properties:
11
- documentContent:
12
- type: string
13
- description: Source code of current document (without the D2 diagram)
14
- required:
15
- - documentContent
16
- output_schema:
17
- type: object
18
- properties:
19
- d2DiagramSourceCode:
20
- type: string
21
- description: Source code to draw D2 diagram
22
- required:
23
- - d2DiagramSourceCode