@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.
- package/.github/workflows/publish-docs.yml +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +19 -0
- package/agents/generate/check-diagram.mjs +40 -0
- package/agents/generate/draw-diagram.yaml +23 -0
- package/agents/generate/generate-structure.yaml +5 -1
- package/agents/generate/merge-d2-diagram.yaml +3 -3
- package/agents/generate/update-document-structure.yaml +5 -2
- package/agents/generate/user-review-document-structure.mjs +7 -0
- package/agents/generate/wrap-diagram-code.mjs +35 -0
- package/agents/history/index.yaml +6 -0
- package/agents/history/view.mjs +75 -0
- package/agents/translate/index.yaml +3 -2
- package/agents/translate/record-translation-history.mjs +19 -0
- package/agents/translate/translate-multilingual.yaml +2 -1
- package/agents/update/batch-update-document.yaml +1 -1
- package/agents/update/check-document.mjs +1 -1
- package/agents/update/fs-tools/read-file.mjs +3 -1
- package/agents/update/generate-document.yaml +31 -25
- package/agents/update/{generate-and-translate-document.yaml → handle-document-update.yaml} +2 -11
- package/agents/update/index.yaml +1 -0
- package/agents/update/save-and-translate-document.mjs +101 -0
- package/agents/update/update-document-detail.yaml +5 -1
- package/agents/update/update-single-document.yaml +1 -10
- package/agents/update/user-review-document.mjs +4 -1
- package/aigne.yaml +8 -1
- package/package.json +1 -1
- package/prompts/detail/d2-diagram/guide.md +19 -0
- package/prompts/detail/d2-diagram/role-and-personality.md +2 -0
- package/prompts/detail/d2-diagram/rules.md +24 -0
- package/prompts/detail/d2-diagram/{rules-system.md → system-prompt.md} +3 -9
- package/prompts/detail/{document-rules.md → generate/document-rules.md} +1 -1
- package/prompts/detail/generate/system-prompt.md +72 -0
- package/prompts/detail/generate/user-prompt.md +54 -0
- package/prompts/detail/{update-document.md → update/system-prompt.md} +43 -67
- package/prompts/detail/update/user-prompt.md +33 -0
- package/prompts/structure/{generate-structure-system.md → generate/system-prompt.md} +7 -40
- package/prompts/structure/{generate-structure-user.md → generate/user-prompt.md} +17 -13
- package/prompts/structure/{update-document-structure.md → update/system-prompt.md} +16 -27
- package/prompts/structure/update/user-prompt.md +19 -0
- package/tests/agents/generate/user-review-document-structure.test.mjs +2 -0
- package/tests/agents/update/check-document.test.mjs +1 -1
- package/tests/agents/update/fs-tools/read-file.test.mjs +8 -12
- package/tests/agents/utils/check-detail-result.test.mjs +13 -0
- package/tests/utils/d2-utils.test.mjs +14 -0
- package/tests/utils/docs-finder-utils.test.mjs +13 -0
- package/tests/utils/history-utils.test.mjs +178 -0
- package/utils/d2-utils.mjs +9 -0
- package/utils/docs-finder-utils.mjs +10 -1
- package/utils/history-utils.mjs +191 -0
- package/utils/markdown-checker.mjs +20 -0
- package/agents/generate/check-d2-diagram-valid.mjs +0 -26
- package/agents/generate/generate-d2-diagram.yaml +0 -23
- package/prompts/detail/generate-document.md +0 -125
- /package/prompts/detail/d2-diagram/{rules-user.md → user-prompt.md} +0 -0
- /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();
|
|
@@ -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:
|
|
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(
|
|
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
|
+
});
|
package/utils/d2-utils.mjs
CHANGED
|
@@ -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
|