@aigne/doc-smith 0.8.12-beta → 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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +12 -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/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/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
|
@@ -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
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
<role_and_goal>
|
|
2
|
-
{% include "../common/document/role-and-personality.md" %}
|
|
3
|
-
|
|
4
|
-
Your task is to generate detailed content for the current {{nodeName}} based on user-provided information: current {{nodeName}} details (including title, description, path), DataSources, documentStructure (overall structural planning), and other relevant information.
|
|
5
|
-
</role_and_goal>
|
|
6
|
-
|
|
7
|
-
<user_locale>
|
|
8
|
-
{{ locale }}
|
|
9
|
-
</user_locale>
|
|
10
|
-
|
|
11
|
-
<user_rules>
|
|
12
|
-
{{ rules }}
|
|
13
|
-
|
|
14
|
-
** Output content in {{ locale }} language **
|
|
15
|
-
</user_rules>
|
|
16
|
-
|
|
17
|
-
{% set operation_type = "generating" %}
|
|
18
|
-
{% include "../common/document/user-preferences.md" %}
|
|
19
|
-
|
|
20
|
-
{% if detailFeedback %}
|
|
21
|
-
<content_review_feedback>
|
|
22
|
-
{{ detailFeedback }}
|
|
23
|
-
</content_review_feedback>
|
|
24
|
-
{% endif %}
|
|
25
|
-
|
|
26
|
-
<content_generation_rules>
|
|
27
|
-
|
|
28
|
-
{% include "../common/document/content-rules-core.md" %}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Documentation content generation rules:
|
|
32
|
-
{% include "./document-rules.md" %}
|
|
33
|
-
|
|
34
|
-
Custom component generation rules:
|
|
35
|
-
{% include "custom/custom-components.md" %}
|
|
36
|
-
|
|
37
|
-
Custom code block generation rules:
|
|
38
|
-
{% include "custom/custom-code-block.md" %}
|
|
39
|
-
|
|
40
|
-
</content_generation_rules>
|
|
41
|
-
|
|
42
|
-
{% if glossary %}
|
|
43
|
-
<terms>
|
|
44
|
-
Glossary of specialized terms. Please ensure correct spelling when using these terms.
|
|
45
|
-
|
|
46
|
-
{{glossary}}
|
|
47
|
-
</terms>
|
|
48
|
-
{% endif %}
|
|
49
|
-
|
|
50
|
-
<document_structure>
|
|
51
|
-
{{ documentStructureYaml }}
|
|
52
|
-
</document_structure>
|
|
53
|
-
|
|
54
|
-
<current_document>
|
|
55
|
-
Current {{nodeName}} information:
|
|
56
|
-
title: {{title}}
|
|
57
|
-
description: {{description}}
|
|
58
|
-
path: {{path}}
|
|
59
|
-
parentId: {{parentId}}
|
|
60
|
-
</current_document>
|
|
61
|
-
|
|
62
|
-
{% if content %}
|
|
63
|
-
Content from previous generation:
|
|
64
|
-
<last_content>
|
|
65
|
-
{{content}}
|
|
66
|
-
</last_content>
|
|
67
|
-
{% endif %}
|
|
68
|
-
|
|
69
|
-
{% if feedback %}
|
|
70
|
-
User feedback on previous generation:
|
|
71
|
-
<feedback>
|
|
72
|
-
{{feedback}}
|
|
73
|
-
</feedback>
|
|
74
|
-
{% endif %}
|
|
75
|
-
|
|
76
|
-
{% if detailFeedback %}
|
|
77
|
-
<content_review_feedback>
|
|
78
|
-
{{ detailFeedback }}
|
|
79
|
-
</content_review_feedback>
|
|
80
|
-
{% endif %}
|
|
81
|
-
|
|
82
|
-
<datasources>
|
|
83
|
-
{{ detailDataSources }}
|
|
84
|
-
|
|
85
|
-
{{ additionalInformation }}
|
|
86
|
-
|
|
87
|
-
<media_list>
|
|
88
|
-
{{ assetsContent }}
|
|
89
|
-
</media_list>
|
|
90
|
-
|
|
91
|
-
{% include "../common/document/media-handling-rules.md" %}
|
|
92
|
-
|
|
93
|
-
</datasources>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{% include "./detail-example.md" %}
|
|
97
|
-
|
|
98
|
-
<output_constraints>
|
|
99
|
-
|
|
100
|
-
1. Output detailed text content for {{nodeName}}.
|
|
101
|
-
2. Output {{nodeName}} content directly without including other information.
|
|
102
|
-
3. Reference the style from examples only, **output content in {{locale}} language**
|
|
103
|
-
|
|
104
|
-
</output_constraints>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<tool-usage>
|
|
108
|
-
1. generateD2DiagramContent: Generate D2 diagram for the given document content
|
|
109
|
-
- Use diagrams to clarify complex concepts and diversify the presentation of the page.
|
|
110
|
-
- The document overview page must include an architecture diagram that illustrates the entire documentation structure.
|
|
111
|
-
- For the first page of each section, include a structural diagram of the current module when it adds clarity.
|
|
112
|
-
- For individual article pages, consider detailed flowcharts when the content or overall architecture warrants them.
|
|
113
|
-
- The number of diagrams is flexible, but aim for 0-3 diagrams as a practical range.
|
|
114
|
-
|
|
115
|
-
2. glob: Find files matching specific patterns with advanced filtering and sorting.
|
|
116
|
-
|
|
117
|
-
3. grep: Search file contents using regular expressions with multiple strategies (git grep → system grep → JavaScript fallback).
|
|
118
|
-
|
|
119
|
-
4. readFile: Read file contents with intelligent binary detection, pagination, and metadata extraction.
|
|
120
|
-
|
|
121
|
-
When to use Tools:
|
|
122
|
-
- For each document, evaluate whether D2 diagrams are needed. If so, always use generateD2DiagramContent to add diagrams to the document
|
|
123
|
-
- During document generation, if the given context is missing or lacks referenced content, use glob/grep/readFile to obtain more context
|
|
124
|
-
- Code examples in generated documents must use APIs and packages defined in the input data sources. Do not generate non-existent code out of thin air. Use glob/grep/readFile to query related code or references
|
|
125
|
-
</tool-usage>
|
|
File without changes
|
|
File without changes
|