@redpanda-data/docs-extensions-and-macros 4.12.6 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.adoc +33 -1064
- package/bin/doc-tools-mcp.js +720 -0
- package/bin/doc-tools.js +1050 -50
- package/bin/mcp-tools/antora.js +153 -0
- package/bin/mcp-tools/cache.js +89 -0
- package/bin/mcp-tools/cloud-regions.js +127 -0
- package/bin/mcp-tools/content-review.js +196 -0
- package/bin/mcp-tools/crd-docs.js +153 -0
- package/bin/mcp-tools/frontmatter.js +138 -0
- package/bin/mcp-tools/generated-docs-review.js +887 -0
- package/bin/mcp-tools/helm-docs.js +152 -0
- package/bin/mcp-tools/index.js +245 -0
- package/bin/mcp-tools/job-queue.js +468 -0
- package/bin/mcp-tools/mcp-validation.js +266 -0
- package/bin/mcp-tools/metrics-docs.js +146 -0
- package/bin/mcp-tools/openapi.js +174 -0
- package/bin/mcp-tools/prompt-discovery.js +283 -0
- package/bin/mcp-tools/property-docs.js +157 -0
- package/bin/mcp-tools/rpcn-docs.js +113 -0
- package/bin/mcp-tools/rpk-docs.js +141 -0
- package/bin/mcp-tools/telemetry.js +211 -0
- package/bin/mcp-tools/utils.js +131 -0
- package/bin/mcp-tools/versions.js +168 -0
- package/cli-utils/convert-doc-links.js +1 -1
- package/cli-utils/github-token.js +58 -0
- package/cli-utils/self-managed-docs-branch.js +2 -2
- package/cli-utils/setup-mcp.js +313 -0
- package/docker-compose/25.1/transactions.md +1 -1
- package/docker-compose/transactions.md +1 -1
- package/extensions/DEVELOPMENT.adoc +464 -0
- package/extensions/README.adoc +124 -0
- package/extensions/REFERENCE.adoc +768 -0
- package/extensions/USER_GUIDE.adoc +339 -0
- package/extensions/generate-rp-connect-info.js +3 -4
- package/extensions/version-fetcher/get-latest-console-version.js +38 -27
- package/extensions/version-fetcher/get-latest-redpanda-version.js +65 -54
- package/extensions/version-fetcher/retry-util.js +88 -0
- package/extensions/version-fetcher/set-latest-version.js +6 -3
- package/macros/DEVELOPMENT.adoc +377 -0
- package/macros/README.adoc +105 -0
- package/macros/REFERENCE.adoc +222 -0
- package/macros/USER_GUIDE.adoc +220 -0
- package/macros/rp-connect-components.js +6 -6
- package/package.json +12 -3
- package/tools/bundle-openapi.js +20 -10
- package/tools/cloud-regions/generate-cloud-regions.js +1 -1
- package/tools/fetch-from-github.js +18 -4
- package/tools/gen-rpk-ascii.py +3 -1
- package/tools/generate-cli-docs.js +325 -0
- package/tools/get-console-version.js +4 -2
- package/tools/get-redpanda-version.js +4 -2
- package/tools/metrics/metrics.py +19 -7
- package/tools/property-extractor/Makefile +7 -1
- package/tools/property-extractor/cloud_config.py +4 -4
- package/tools/property-extractor/constant_resolver.py +11 -11
- package/tools/property-extractor/property_extractor.py +18 -16
- package/tools/property-extractor/topic_property_extractor.py +2 -2
- package/tools/property-extractor/transformers.py +7 -7
- package/tools/property-extractor/type_definition_extractor.py +4 -4
- package/tools/redpanda-connect/README.adoc +1 -1
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +5 -3
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tools - Documentation Review
|
|
3
|
+
*
|
|
4
|
+
* OPTIMIZATION: This tool uses programmatic checks instead of LLM for most validations.
|
|
5
|
+
* - Caches style guide content
|
|
6
|
+
* - Programmatic checks for: missing descriptions, invalid xrefs, DRY violations
|
|
7
|
+
* - Only style/tone review requires LLM processing
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { findRepoRoot, formatDate } = require('./utils');
|
|
13
|
+
const cache = require('./cache');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sanitize version string to prevent path traversal
|
|
17
|
+
* @param {string} version - Version string to sanitize
|
|
18
|
+
* @returns {string} Sanitized version string
|
|
19
|
+
* @throws {Error} If version contains invalid characters
|
|
20
|
+
*/
|
|
21
|
+
function sanitizeVersion(version) {
|
|
22
|
+
if (!version || typeof version !== 'string') {
|
|
23
|
+
throw new Error('Version must be a non-empty string');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Reject path separators
|
|
27
|
+
if (version.includes('/') || version.includes('\\')) {
|
|
28
|
+
throw new Error('Version cannot contain path separators (/ or \\)');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Reject path traversal sequences
|
|
32
|
+
if (version.includes('..')) {
|
|
33
|
+
throw new Error('Version cannot contain path traversal sequences (..)');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Reject null bytes
|
|
37
|
+
if (version.includes('\0')) {
|
|
38
|
+
throw new Error('Version cannot contain null bytes');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Whitelist: only allow alphanumeric, dots, dashes, and underscores
|
|
42
|
+
const sanitized = version.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
43
|
+
|
|
44
|
+
// Ensure the sanitized version is not empty and not too long
|
|
45
|
+
if (sanitized.length === 0) {
|
|
46
|
+
throw new Error('Version contains only invalid characters');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (sanitized.length > 100) {
|
|
50
|
+
throw new Error('Version string is too long (max 100 characters)');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return sanitized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build style review instructions for LLM-based review
|
|
58
|
+
* @param {string} docType - Type of documentation being reviewed
|
|
59
|
+
* @returns {string} Review instructions
|
|
60
|
+
*/
|
|
61
|
+
function buildStyleReviewInstructions(docType) {
|
|
62
|
+
const baseInstructions = `
|
|
63
|
+
# Style Guide Review Instructions
|
|
64
|
+
|
|
65
|
+
You are reviewing autogenerated ${docType} documentation for Redpanda.
|
|
66
|
+
|
|
67
|
+
## Your Task
|
|
68
|
+
|
|
69
|
+
Review the content sample below against the style guide and identify:
|
|
70
|
+
|
|
71
|
+
1. **Terminology issues**
|
|
72
|
+
- Incorrect capitalization (e.g., "RedPanda" should be "Redpanda")
|
|
73
|
+
- Deprecated terms (e.g., "whitelist/blacklist" should be "allowlist/denylist")
|
|
74
|
+
- Incorrect technical terms
|
|
75
|
+
|
|
76
|
+
2. **Voice and tone issues**
|
|
77
|
+
- Passive voice (should be active)
|
|
78
|
+
- Wrong tense (should be present tense)
|
|
79
|
+
- Wrong person (should be second person "you")
|
|
80
|
+
|
|
81
|
+
3. **Formatting issues**
|
|
82
|
+
- Heading capitalization (should be sentence case, not title case)
|
|
83
|
+
- Overuse of bold
|
|
84
|
+
- Use of em dashes (should use parentheses or colons instead)
|
|
85
|
+
|
|
86
|
+
4. **Structural issues**
|
|
87
|
+
- Incorrect heading hierarchy
|
|
88
|
+
- Missing descriptions or context
|
|
89
|
+
- Unclear explanations
|
|
90
|
+
|
|
91
|
+
## Output Format
|
|
92
|
+
|
|
93
|
+
For each issue found, provide:
|
|
94
|
+
- Line number or section reference
|
|
95
|
+
- Issue type (terminology/voice/formatting/structure)
|
|
96
|
+
- Specific problem
|
|
97
|
+
- Suggested fix
|
|
98
|
+
|
|
99
|
+
If no issues are found in a category, state "No issues found."
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
return baseInstructions;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate an AsciiDoc report from review results
|
|
107
|
+
* @param {Object} results - Review results
|
|
108
|
+
* @param {string} outputPath - Path to save the report
|
|
109
|
+
* @returns {string} Generated report content
|
|
110
|
+
*/
|
|
111
|
+
function generateReviewReport(results, outputPath) {
|
|
112
|
+
const { doc_type, version, quality_score, issues, suggestions, files_analyzed } = results;
|
|
113
|
+
|
|
114
|
+
const errorIssues = issues.filter(i => i.severity === 'error');
|
|
115
|
+
const warningIssues = issues.filter(i => i.severity === 'warning');
|
|
116
|
+
const infoIssues = issues.filter(i => i.severity === 'info');
|
|
117
|
+
|
|
118
|
+
let report = `= Documentation Review Report\n\n`;
|
|
119
|
+
report += `[cols="1,3"]\n`;
|
|
120
|
+
report += `|===\n`;
|
|
121
|
+
report += `| Type | ${doc_type}\n`;
|
|
122
|
+
report += `| Version | ${version || 'N/A'}\n`;
|
|
123
|
+
report += `| Date | ${formatDate()}\n`;
|
|
124
|
+
report += `| Files Analyzed | ${files_analyzed}\n`;
|
|
125
|
+
report += `|===\n\n`;
|
|
126
|
+
|
|
127
|
+
report += `== Quality Score: ${quality_score}/100\n\n`;
|
|
128
|
+
|
|
129
|
+
// Score interpretation
|
|
130
|
+
if (quality_score >= 90) {
|
|
131
|
+
report += `[NOTE]\n====\n*Excellent* - Documentation quality is very high.\n====\n\n`;
|
|
132
|
+
} else if (quality_score >= 75) {
|
|
133
|
+
report += `[WARNING]\n====\n*Good* - Documentation quality is acceptable but has room for improvement.\n====\n\n`;
|
|
134
|
+
} else if (quality_score >= 50) {
|
|
135
|
+
report += `[WARNING]\n====\n*Fair* - Documentation needs improvement in several areas.\n====\n\n`;
|
|
136
|
+
} else {
|
|
137
|
+
report += `[CAUTION]\n====\n*Poor* - Documentation requires significant improvements.\n====\n\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Scoring breakdown with detailed calculation
|
|
141
|
+
report += `=== Scoring Breakdown\n\n`;
|
|
142
|
+
report += `*How the score is calculated:*\n\n`;
|
|
143
|
+
|
|
144
|
+
let runningScore = 100;
|
|
145
|
+
report += `. Starting score: *${runningScore}*\n`;
|
|
146
|
+
|
|
147
|
+
if (errorIssues.length > 0) {
|
|
148
|
+
const errorDeduction = errorIssues.reduce((sum, issue) => {
|
|
149
|
+
if (issue.issue.includes('enterprise license') || issue.issue.includes('cloud-specific')) return sum + 5;
|
|
150
|
+
if (issue.issue.includes('invalid xref')) return sum + 3;
|
|
151
|
+
return sum + 3;
|
|
152
|
+
}, 0);
|
|
153
|
+
runningScore -= errorDeduction;
|
|
154
|
+
report += `. Errors: ${errorIssues.length} issues × 3-5 points = -*${errorDeduction}* (now ${runningScore})\n`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const missingDescCount = warningIssues.filter(i => i.issue === 'Missing description').length;
|
|
158
|
+
const shortDescCount = infoIssues.filter(i => i.issue.includes('Very short description')).length;
|
|
159
|
+
const exampleCount = infoIssues.filter(i => i.issue.includes('would benefit from an example')).length;
|
|
160
|
+
const otherWarningCount = warningIssues.length - missingDescCount;
|
|
161
|
+
|
|
162
|
+
if (missingDescCount > 0) {
|
|
163
|
+
const deduction = Math.min(20, missingDescCount * 2);
|
|
164
|
+
runningScore -= deduction;
|
|
165
|
+
report += `. Missing descriptions: ${missingDescCount} issues × 2 points = -*${deduction}* (capped at 20, now ${runningScore})\n`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (shortDescCount > 0) {
|
|
169
|
+
const deduction = Math.min(10, shortDescCount);
|
|
170
|
+
runningScore -= deduction;
|
|
171
|
+
report += `. Short descriptions: ${shortDescCount} issues × 1 point = -*${deduction}* (capped at 10, now ${runningScore})\n`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (exampleCount > 0) {
|
|
175
|
+
const deduction = Math.min(5, Math.floor(exampleCount / 5));
|
|
176
|
+
runningScore -= deduction;
|
|
177
|
+
report += `. Missing examples: ${exampleCount} complex properties = -*${deduction}* (1 point per 5 properties, capped at 5, now ${runningScore})\n`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (otherWarningCount > 0) {
|
|
181
|
+
const deduction = Math.min(otherWarningCount * 2, 10);
|
|
182
|
+
runningScore -= deduction;
|
|
183
|
+
report += `. Other warnings: ${otherWarningCount} issues × 1-2 points = -*${deduction}* (now ${runningScore})\n`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
report += `\n*Final Score: ${quality_score}/100*\n\n`;
|
|
187
|
+
|
|
188
|
+
// Summary
|
|
189
|
+
report += `== Summary\n\n`;
|
|
190
|
+
report += `* *Total Issues:* ${issues.length}\n`;
|
|
191
|
+
report += `** Errors: ${errorIssues.length}\n`;
|
|
192
|
+
report += `** Warnings: ${warningIssues.length}\n`;
|
|
193
|
+
report += `** Info: ${infoIssues.length}\n\n`;
|
|
194
|
+
|
|
195
|
+
// General suggestions
|
|
196
|
+
if (suggestions.length > 0) {
|
|
197
|
+
report += `=== Key Findings\n\n`;
|
|
198
|
+
suggestions.forEach(s => {
|
|
199
|
+
report += `* ${s}\n`;
|
|
200
|
+
});
|
|
201
|
+
report += `\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Errors (highest priority)
|
|
205
|
+
if (errorIssues.length > 0) {
|
|
206
|
+
report += `== Errors (high priority)\n\n`;
|
|
207
|
+
report += `These issues violate documentation standards and should be fixed immediately.\n\n`;
|
|
208
|
+
|
|
209
|
+
errorIssues.forEach((issue, idx) => {
|
|
210
|
+
report += `=== ${idx + 1}. ${issue.property || issue.path || 'General'}\n\n`;
|
|
211
|
+
report += `*Issue:* ${issue.issue}\n\n`;
|
|
212
|
+
if (issue.suggestion) {
|
|
213
|
+
report += `*Fix:* ${issue.suggestion}\n\n`;
|
|
214
|
+
}
|
|
215
|
+
report += `*File:* \`${issue.file}\`\n\n`;
|
|
216
|
+
|
|
217
|
+
// Add specific instructions based on issue type
|
|
218
|
+
if (issue.issue.includes('enterprise license')) {
|
|
219
|
+
report += `*Action:*\n\n`;
|
|
220
|
+
report += `. Open \`docs-data/property-overrides.json\`\n`;
|
|
221
|
+
report += `. Find property \`${issue.property}\`\n`;
|
|
222
|
+
report += `. Remove the \`include::reference:partial$enterprise-licensed-property.adoc[]\` from the description\n`;
|
|
223
|
+
report += `. Regenerate docs\n\n`;
|
|
224
|
+
} else if (issue.issue.includes('cloud-specific conditional')) {
|
|
225
|
+
report += `*Action:*\n\n`;
|
|
226
|
+
report += `. Open \`docs-data/property-overrides.json\`\n`;
|
|
227
|
+
report += `. Find property \`${issue.property}\`\n`;
|
|
228
|
+
report += `. Remove the \`ifdef::env-cloud\` blocks from the description\n`;
|
|
229
|
+
report += `. Cloud-specific info will appear in metadata automatically\n`;
|
|
230
|
+
report += `. Regenerate docs\n\n`;
|
|
231
|
+
} else if (issue.issue.includes('invalid xref')) {
|
|
232
|
+
report += `*Action:*\n\n`;
|
|
233
|
+
report += `. Open \`docs-data/property-overrides.json\`\n`;
|
|
234
|
+
report += `. Find property \`${issue.property}\`\n`;
|
|
235
|
+
report += `. Update xref links to use full Antora resource IDs\n`;
|
|
236
|
+
report += `. Example: \`xref:reference:properties/cluster-properties.adoc[Link]\`\n`;
|
|
237
|
+
report += `. Regenerate docs\n\n`;
|
|
238
|
+
} else if (issue.issue.includes('Invalid $ref')) {
|
|
239
|
+
report += `*Action:*\n\n`;
|
|
240
|
+
report += `. Open \`docs-data/overrides.json\`\n`;
|
|
241
|
+
report += `. Find the reference at \`${issue.path}\`\n`;
|
|
242
|
+
report += `. Either add the missing definition or fix the reference\n`;
|
|
243
|
+
report += `. Regenerate docs\n\n`;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Warnings
|
|
249
|
+
if (warningIssues.length > 0) {
|
|
250
|
+
report += `== Warnings\n\n`;
|
|
251
|
+
report += `These issues should be addressed to improve documentation quality.\n\n`;
|
|
252
|
+
|
|
253
|
+
// Group warnings by issue type
|
|
254
|
+
const warningsByType = {};
|
|
255
|
+
warningIssues.forEach(issue => {
|
|
256
|
+
const issueType = issue.issue.split(':')[0] || issue.issue;
|
|
257
|
+
if (!warningsByType[issueType]) {
|
|
258
|
+
warningsByType[issueType] = [];
|
|
259
|
+
}
|
|
260
|
+
warningsByType[issueType].push(issue);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
Object.entries(warningsByType).forEach(([type, typeIssues]) => {
|
|
264
|
+
report += `=== ${type} (${typeIssues.length})\n\n`;
|
|
265
|
+
|
|
266
|
+
if (type === 'Missing description') {
|
|
267
|
+
report += `*Fix:* Add descriptions to these properties in \`docs-data/property-overrides.json\`\n\n`;
|
|
268
|
+
report += `*Properties needing descriptions:*\n\n`;
|
|
269
|
+
typeIssues.forEach(issue => {
|
|
270
|
+
report += `* \`${issue.property}\`\n`;
|
|
271
|
+
});
|
|
272
|
+
report += `\n`;
|
|
273
|
+
} else {
|
|
274
|
+
typeIssues.forEach(issue => {
|
|
275
|
+
report += `* *${issue.property || issue.path}*: ${issue.issue}\n`;
|
|
276
|
+
});
|
|
277
|
+
report += `\n`;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Info items
|
|
283
|
+
if (infoIssues.length > 0) {
|
|
284
|
+
report += `== Info\n\n`;
|
|
285
|
+
report += `These are suggestions for enhancement.\n\n`;
|
|
286
|
+
|
|
287
|
+
// Group by issue type
|
|
288
|
+
const infoByType = {};
|
|
289
|
+
infoIssues.forEach(issue => {
|
|
290
|
+
const issueType = issue.issue.split('(')[0].trim();
|
|
291
|
+
if (!infoByType[issueType]) {
|
|
292
|
+
infoByType[issueType] = [];
|
|
293
|
+
}
|
|
294
|
+
infoByType[issueType].push(issue);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
Object.entries(infoByType).forEach(([type, typeIssues]) => {
|
|
298
|
+
report += `=== ${type}\n\n`;
|
|
299
|
+
typeIssues.forEach(issue => {
|
|
300
|
+
report += `* *${issue.property || issue.path}*: ${issue.issue}\n`;
|
|
301
|
+
});
|
|
302
|
+
report += `\n`;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Next steps
|
|
307
|
+
report += `== Next Steps\n\n`;
|
|
308
|
+
if (errorIssues.length > 0) {
|
|
309
|
+
report += `. *Fix errors first* - Address the ${errorIssues.length} error(s) above\n`;
|
|
310
|
+
}
|
|
311
|
+
if (warningIssues.length > 0) {
|
|
312
|
+
report += `${errorIssues.length > 0 ? '. ' : '. '}*Review warnings* - Prioritize the ${warningIssues.length} warning(s)\n`;
|
|
313
|
+
}
|
|
314
|
+
const step = errorIssues.length > 0 && warningIssues.length > 0 ? 3 : errorIssues.length > 0 || warningIssues.length > 0 ? 2 : 1;
|
|
315
|
+
report += `${step > 1 ? '. ' : '. '}*Regenerate documentation* - After making changes, regenerate the docs\n`;
|
|
316
|
+
report += `. *Review again* - Run the review tool again to verify fixes\n\n`;
|
|
317
|
+
|
|
318
|
+
// Write report
|
|
319
|
+
fs.writeFileSync(outputPath, report, 'utf8');
|
|
320
|
+
|
|
321
|
+
return report;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Review generated documentation for quality issues
|
|
326
|
+
* @param {Object} args - Arguments
|
|
327
|
+
* @param {string} args.doc_type - Type of docs to review (properties, metrics, rpk, rpcn_connectors)
|
|
328
|
+
* @param {string} args.version - Version of the docs to review (for properties, metrics, rpk)
|
|
329
|
+
* @param {boolean} [args.generate_report] - Whether to generate a markdown report file
|
|
330
|
+
* @param {string} [args.properties_dir] - Custom path to properties directory (default: modules/reference)
|
|
331
|
+
* @param {string} [args.metrics_file] - Custom path to metrics file (default: modules/reference/pages/public-metrics-reference.adoc)
|
|
332
|
+
* @param {string} [args.rpk_dir] - Custom path to RPK docs directory (default: autogenerated/{version}/rpk)
|
|
333
|
+
* @param {string} [args.overrides_file] - Custom path to overrides.json for RPCN connectors (default: docs-data/overrides.json)
|
|
334
|
+
* @returns {Object} Review results with issues and suggestions
|
|
335
|
+
*/
|
|
336
|
+
function reviewGeneratedDocs(args) {
|
|
337
|
+
const repoRoot = findRepoRoot();
|
|
338
|
+
const { doc_type, version, generate_report } = args;
|
|
339
|
+
|
|
340
|
+
// Load style guide from cache or file (cache for 1 hour)
|
|
341
|
+
const styleGuidePath = path.join(repoRoot.root, 'mcp', 'team-standards', 'style-guide.md');
|
|
342
|
+
let styleGuide = null;
|
|
343
|
+
|
|
344
|
+
const cacheKey = 'style-guide-content';
|
|
345
|
+
styleGuide = cache.get(cacheKey);
|
|
346
|
+
|
|
347
|
+
if (!styleGuide && fs.existsSync(styleGuidePath)) {
|
|
348
|
+
try {
|
|
349
|
+
styleGuide = fs.readFileSync(styleGuidePath, 'utf8');
|
|
350
|
+
cache.set(cacheKey, styleGuide, 60 * 60 * 1000); // Cache for 1 hour
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.error(`Warning: Could not load style guide: ${err.message}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!doc_type) {
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error: 'doc_type is required',
|
|
360
|
+
suggestion: 'Provide one of: properties, metrics, rpk, rpcn_connectors'
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const issues = [];
|
|
365
|
+
const suggestions = [];
|
|
366
|
+
let filesAnalyzed = 0;
|
|
367
|
+
let qualityScore = 100;
|
|
368
|
+
let contentSample = ''; // For storing content samples for LLM review
|
|
369
|
+
let contentFilePath = null; // Path to full content file for deep review if needed
|
|
370
|
+
let contentTotalLines = 0; // Total lines in the content file
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
switch (doc_type) {
|
|
374
|
+
case 'properties': {
|
|
375
|
+
if (!version) {
|
|
376
|
+
return {
|
|
377
|
+
success: false,
|
|
378
|
+
error: 'version is required for property docs review'
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Sanitize version to prevent path traversal
|
|
383
|
+
let sanitizedVersion;
|
|
384
|
+
try {
|
|
385
|
+
sanitizedVersion = sanitizeVersion(version);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return {
|
|
388
|
+
success: false,
|
|
389
|
+
error: `Invalid version: ${err.message}`
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Normalize version
|
|
394
|
+
let normalizedVersion = sanitizedVersion;
|
|
395
|
+
if (!normalizedVersion.startsWith('v') && normalizedVersion !== 'latest') {
|
|
396
|
+
normalizedVersion = `v${normalizedVersion}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check for generated JSON file (use custom path or default)
|
|
400
|
+
const propertiesBaseDir = args.properties_dir || path.join('modules', 'reference');
|
|
401
|
+
const jsonPath = path.join(repoRoot.root, propertiesBaseDir, 'attachments', `redpanda-properties-${normalizedVersion}.json`);
|
|
402
|
+
if (!fs.existsSync(jsonPath)) {
|
|
403
|
+
return {
|
|
404
|
+
success: false,
|
|
405
|
+
error: `Properties JSON not found at ${jsonPath}`,
|
|
406
|
+
suggestion: 'Generate property docs first using generate_property_docs tool'
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
filesAnalyzed++;
|
|
411
|
+
|
|
412
|
+
// Read and parse the properties JSON
|
|
413
|
+
const propertiesData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
414
|
+
const allProperties = Object.values(propertiesData.properties || {});
|
|
415
|
+
|
|
416
|
+
// Properties that typically benefit from examples
|
|
417
|
+
const shouldHaveExample = (prop) => {
|
|
418
|
+
const name = prop.name.toLowerCase();
|
|
419
|
+
// Properties with specific formats, complex values, or commonly misconfigured
|
|
420
|
+
return name.includes('pattern') ||
|
|
421
|
+
name.includes('regex') ||
|
|
422
|
+
name.includes('format') ||
|
|
423
|
+
name.includes('template') ||
|
|
424
|
+
name.includes('config') ||
|
|
425
|
+
name.includes('override') ||
|
|
426
|
+
name.includes('mapping') ||
|
|
427
|
+
name.includes('filter') ||
|
|
428
|
+
name.includes('selector') ||
|
|
429
|
+
(prop.type && prop.type.includes('array')) ||
|
|
430
|
+
(prop.type && prop.type.includes('object'));
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Check for missing or short descriptions
|
|
434
|
+
let missingDescriptions = 0;
|
|
435
|
+
let shortDescriptions = 0;
|
|
436
|
+
let emptyDefaults = 0;
|
|
437
|
+
let missingExamples = 0;
|
|
438
|
+
|
|
439
|
+
allProperties.forEach(prop => {
|
|
440
|
+
if (!prop.description || prop.description.trim() === '') {
|
|
441
|
+
missingDescriptions++;
|
|
442
|
+
if (!prop.is_deprecated) {
|
|
443
|
+
issues.push({
|
|
444
|
+
severity: 'warning',
|
|
445
|
+
file: jsonPath,
|
|
446
|
+
property: prop.name,
|
|
447
|
+
issue: 'Missing description'
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} else if (prop.description.length < 20 && !prop.is_deprecated) {
|
|
451
|
+
shortDescriptions++;
|
|
452
|
+
issues.push({
|
|
453
|
+
severity: 'info',
|
|
454
|
+
file: jsonPath,
|
|
455
|
+
property: prop.name,
|
|
456
|
+
issue: `Very short description (${prop.description.length} chars): "${prop.description}"`
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if ((!prop.default || (typeof prop.default === 'string' && prop.default.trim() === '')) && !prop.is_deprecated && prop.config_scope !== 'broker') {
|
|
461
|
+
emptyDefaults++;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Track properties that should have examples
|
|
465
|
+
if (shouldHaveExample(prop) && !prop.is_deprecated) {
|
|
466
|
+
missingExamples++;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Check ALL properties for missing examples
|
|
471
|
+
const propertiesNeedingExamples = [];
|
|
472
|
+
const overridesPath = path.join(repoRoot.root, 'docs-data', 'property-overrides.json');
|
|
473
|
+
const overrides = fs.existsSync(overridesPath) ? JSON.parse(fs.readFileSync(overridesPath, 'utf8')) : { properties: {} };
|
|
474
|
+
|
|
475
|
+
allProperties.forEach(prop => {
|
|
476
|
+
if (shouldHaveExample(prop) && !prop.is_deprecated) {
|
|
477
|
+
const override = overrides.properties && overrides.properties[prop.name];
|
|
478
|
+
if (!override || !override.example) {
|
|
479
|
+
propertiesNeedingExamples.push(prop.name);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Read property overrides to check for quality issues
|
|
485
|
+
if (fs.existsSync(overridesPath)) {
|
|
486
|
+
filesAnalyzed++;
|
|
487
|
+
|
|
488
|
+
if (overrides.properties) {
|
|
489
|
+
Object.entries(overrides.properties).forEach(([propName, override]) => {
|
|
490
|
+
let propData = allProperties.find(p => p.name === propName);
|
|
491
|
+
|
|
492
|
+
// Check for enterprise license includes (should not be in descriptions)
|
|
493
|
+
if (override.description && override.description.includes('include::reference:partial$enterprise-licensed-property.adoc')) {
|
|
494
|
+
issues.push({
|
|
495
|
+
severity: 'error',
|
|
496
|
+
file: overridesPath,
|
|
497
|
+
property: propName,
|
|
498
|
+
issue: 'Description contains enterprise license include (should be in metadata only)',
|
|
499
|
+
suggestion: 'Remove the include statement from the description'
|
|
500
|
+
});
|
|
501
|
+
qualityScore -= 5;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Check for cloud-specific conditional blocks
|
|
505
|
+
if (override.description && (override.description.includes('ifdef::env-cloud') || override.description.includes('ifndef::env-cloud'))) {
|
|
506
|
+
issues.push({
|
|
507
|
+
severity: 'error',
|
|
508
|
+
file: overridesPath,
|
|
509
|
+
property: propName,
|
|
510
|
+
issue: 'Description contains cloud-specific conditional blocks',
|
|
511
|
+
suggestion: 'Remove cloud conditionals - this info belongs in metadata'
|
|
512
|
+
});
|
|
513
|
+
qualityScore -= 5;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Check for deprecated properties with descriptions (should not have overrides)
|
|
517
|
+
if (!propData) propData = allProperties.find(p => p.name === propName);
|
|
518
|
+
if (propData && propData.is_deprecated && override.description) {
|
|
519
|
+
issues.push({
|
|
520
|
+
severity: 'warning',
|
|
521
|
+
file: overridesPath,
|
|
522
|
+
property: propName,
|
|
523
|
+
issue: 'Override exists for deprecated property',
|
|
524
|
+
suggestion: 'Remove override for deprecated properties'
|
|
525
|
+
});
|
|
526
|
+
qualityScore -= 2;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Check for invalid xref links (not using full Antora resource IDs)
|
|
530
|
+
if (override.description) {
|
|
531
|
+
const invalidXrefPattern = /xref:\.\/|xref:(?![\w-]+:)/g;
|
|
532
|
+
const invalidXrefs = override.description.match(invalidXrefPattern);
|
|
533
|
+
if (invalidXrefs) {
|
|
534
|
+
issues.push({
|
|
535
|
+
severity: 'error',
|
|
536
|
+
file: overridesPath,
|
|
537
|
+
property: propName,
|
|
538
|
+
issue: 'Description contains invalid xref links (not using full Antora resource IDs)',
|
|
539
|
+
suggestion: 'Use full resource IDs like xref:reference:path/to/doc.adoc[Link]'
|
|
540
|
+
});
|
|
541
|
+
qualityScore -= 3;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Check for duplicate links in related_topics
|
|
546
|
+
if (override.related_topics && Array.isArray(override.related_topics)) {
|
|
547
|
+
const uniqueLinks = new Set(override.related_topics);
|
|
548
|
+
if (uniqueLinks.size < override.related_topics.length) {
|
|
549
|
+
issues.push({
|
|
550
|
+
severity: 'warning',
|
|
551
|
+
file: overridesPath,
|
|
552
|
+
property: propName,
|
|
553
|
+
issue: 'Duplicate links in related_topics',
|
|
554
|
+
suggestion: 'Remove duplicate links'
|
|
555
|
+
});
|
|
556
|
+
qualityScore -= 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Add summary suggestions
|
|
564
|
+
if (missingDescriptions > 0) {
|
|
565
|
+
suggestions.push(`${missingDescriptions} properties have missing descriptions`);
|
|
566
|
+
qualityScore -= Math.min(20, missingDescriptions * 2);
|
|
567
|
+
}
|
|
568
|
+
if (shortDescriptions > 0) {
|
|
569
|
+
suggestions.push(`${shortDescriptions} properties have very short descriptions (< 20 chars)`);
|
|
570
|
+
qualityScore -= Math.min(10, shortDescriptions);
|
|
571
|
+
}
|
|
572
|
+
if (emptyDefaults > 0) {
|
|
573
|
+
suggestions.push(`${emptyDefaults} non-deprecated properties have no default value listed`);
|
|
574
|
+
}
|
|
575
|
+
if (propertiesNeedingExamples.length > 0) {
|
|
576
|
+
suggestions.push(`${propertiesNeedingExamples.length} complex properties would benefit from examples`);
|
|
577
|
+
// Add info-level issues for properties that should have examples
|
|
578
|
+
propertiesNeedingExamples.forEach(propName => {
|
|
579
|
+
issues.push({
|
|
580
|
+
severity: 'info',
|
|
581
|
+
file: overridesPath,
|
|
582
|
+
property: propName,
|
|
583
|
+
issue: 'Complex property would benefit from an example',
|
|
584
|
+
suggestion: 'Add an example array to the property override showing typical usage'
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
qualityScore -= Math.min(5, Math.floor(propertiesNeedingExamples.length / 5));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Sample generated property docs for style review (hybrid approach)
|
|
591
|
+
const clusterPropsPath = path.join(repoRoot.root, propertiesBaseDir, 'partials', 'properties', 'cluster-properties.adoc');
|
|
592
|
+
if (fs.existsSync(clusterPropsPath)) {
|
|
593
|
+
const fullContent = fs.readFileSync(clusterPropsPath, 'utf8');
|
|
594
|
+
const lines = fullContent.split('\n');
|
|
595
|
+
// Sample first 300 lines to catch systematic issues
|
|
596
|
+
// (enough to see multiple properties and their patterns)
|
|
597
|
+
contentSample = lines.slice(0, 300).join('\n');
|
|
598
|
+
// Store file path for full review if needed
|
|
599
|
+
contentFilePath = clusterPropsPath;
|
|
600
|
+
contentTotalLines = lines.length;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
case 'rpcn_connectors': {
|
|
607
|
+
// Read overrides.json (use custom path or default)
|
|
608
|
+
const overridesPath = args.overrides_file ?
|
|
609
|
+
path.join(repoRoot.root, args.overrides_file) :
|
|
610
|
+
path.join(repoRoot.root, 'docs-data', 'overrides.json');
|
|
611
|
+
|
|
612
|
+
if (!fs.existsSync(overridesPath)) {
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: `overrides.json not found at ${overridesPath}`,
|
|
616
|
+
suggestion: 'Generate RPCN connector docs first using generate_rpcn_connector_docs tool, or specify custom path with overrides_file parameter'
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
filesAnalyzed++;
|
|
621
|
+
const overrides = JSON.parse(fs.readFileSync(overridesPath, 'utf8'));
|
|
622
|
+
|
|
623
|
+
// Validate $ref references
|
|
624
|
+
const definitions = overrides.definitions || {};
|
|
625
|
+
const allRefs = new Set();
|
|
626
|
+
const invalidRefs = [];
|
|
627
|
+
|
|
628
|
+
const findRefs = (obj, path = '') => {
|
|
629
|
+
if (typeof obj !== 'object' || obj === null) return;
|
|
630
|
+
|
|
631
|
+
if (obj.$ref) {
|
|
632
|
+
allRefs.add(obj.$ref);
|
|
633
|
+
// Check if ref is valid
|
|
634
|
+
const refPath = obj.$ref.replace('#/definitions/', '');
|
|
635
|
+
if (!definitions[refPath]) {
|
|
636
|
+
invalidRefs.push({
|
|
637
|
+
ref: obj.$ref,
|
|
638
|
+
path
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
644
|
+
if (key !== '$ref') {
|
|
645
|
+
findRefs(value, path ? `${path}.${key}` : key);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
['inputs', 'outputs', 'processors', 'caches'].forEach(section => {
|
|
651
|
+
if (overrides[section]) {
|
|
652
|
+
findRefs(overrides[section], section);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
invalidRefs.forEach(({ ref, path }) => {
|
|
657
|
+
issues.push({
|
|
658
|
+
severity: 'error',
|
|
659
|
+
file: overridesPath,
|
|
660
|
+
path,
|
|
661
|
+
issue: `Invalid $ref: ${ref}`,
|
|
662
|
+
suggestion: 'Ensure the reference exists in the definitions section'
|
|
663
|
+
});
|
|
664
|
+
qualityScore -= 5;
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Check for duplicate descriptions (DRY violations)
|
|
668
|
+
const descriptions = new Map();
|
|
669
|
+
const checkDuplicates = (obj, path = '') => {
|
|
670
|
+
if (typeof obj !== 'object' || obj === null) return;
|
|
671
|
+
|
|
672
|
+
if (obj.description && !obj.$ref && typeof obj.description === 'string' && obj.description.length > 30) {
|
|
673
|
+
const key = obj.description.trim().toLowerCase();
|
|
674
|
+
if (descriptions.has(key)) {
|
|
675
|
+
descriptions.get(key).push(path);
|
|
676
|
+
} else {
|
|
677
|
+
descriptions.set(key, [path]);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
682
|
+
checkDuplicates(value, path ? `${path}.${key}` : key);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
['inputs', 'outputs', 'processors', 'caches'].forEach(section => {
|
|
687
|
+
if (overrides[section]) {
|
|
688
|
+
checkDuplicates(overrides[section], section);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const duplicates = Array.from(descriptions.entries()).filter(([_, paths]) => paths.length > 1);
|
|
693
|
+
duplicates.forEach(([desc, paths]) => {
|
|
694
|
+
suggestions.push(`Duplicate description found at: ${paths.join(', ')}. Consider creating a definition and using $ref`);
|
|
695
|
+
qualityScore -= 3;
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
if (invalidRefs.length === 0 && duplicates.length === 0) {
|
|
699
|
+
suggestions.push('All $ref references are valid and DRY principles are maintained');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Sample connector descriptions for style review
|
|
703
|
+
// Collect sample descriptions from inputs/outputs
|
|
704
|
+
const sampleDescriptions = [];
|
|
705
|
+
['inputs', 'outputs'].forEach(section => {
|
|
706
|
+
if (overrides[section]) {
|
|
707
|
+
Object.entries(overrides[section]).slice(0, 5).forEach(([name, config]) => {
|
|
708
|
+
if (config.description) {
|
|
709
|
+
sampleDescriptions.push(`\n== ${name} (${section})\n\n${config.description}`);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
if (sampleDescriptions.length > 0) {
|
|
716
|
+
contentSample = `= RPCN Connector Descriptions Sample\n${sampleDescriptions.join('\n\n')}`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
case 'metrics':
|
|
723
|
+
case 'rpk': {
|
|
724
|
+
// For metrics and RPK, check files exist and sample content for style review
|
|
725
|
+
if (!version) {
|
|
726
|
+
return {
|
|
727
|
+
success: false,
|
|
728
|
+
error: 'version is required for metrics/rpk docs review'
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Sanitize version to prevent path traversal
|
|
733
|
+
let sanitizedVersion;
|
|
734
|
+
try {
|
|
735
|
+
sanitizedVersion = sanitizeVersion(version);
|
|
736
|
+
} catch (err) {
|
|
737
|
+
return {
|
|
738
|
+
success: false,
|
|
739
|
+
error: `Invalid version: ${err.message}`
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
let filePath;
|
|
744
|
+
|
|
745
|
+
if (doc_type === 'metrics') {
|
|
746
|
+
// Use custom path or default
|
|
747
|
+
filePath = args.metrics_file ?
|
|
748
|
+
path.join(repoRoot.root, args.metrics_file) :
|
|
749
|
+
path.join(repoRoot.root, 'modules', 'reference', 'pages', 'public-metrics-reference.adoc');
|
|
750
|
+
|
|
751
|
+
if (!fs.existsSync(filePath)) {
|
|
752
|
+
return {
|
|
753
|
+
success: false,
|
|
754
|
+
error: `Generated docs not found at ${filePath}`,
|
|
755
|
+
suggestion: 'Generate metrics docs first using generate_metrics_docs tool, or specify custom path with metrics_file parameter'
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Read first 300 lines as sample for style review (hybrid approach)
|
|
760
|
+
const fullContent = fs.readFileSync(filePath, 'utf8');
|
|
761
|
+
const lines = fullContent.split('\n');
|
|
762
|
+
contentSample = lines.slice(0, 300).join('\n');
|
|
763
|
+
contentFilePath = filePath;
|
|
764
|
+
contentTotalLines = lines.length;
|
|
765
|
+
|
|
766
|
+
} else {
|
|
767
|
+
// RPK files are version-specific
|
|
768
|
+
const normalizedVersion = sanitizedVersion.startsWith('v') ? sanitizedVersion : `v${sanitizedVersion}`;
|
|
769
|
+
// Use custom path or default
|
|
770
|
+
const rpkDir = args.rpk_dir ?
|
|
771
|
+
path.join(repoRoot.root, args.rpk_dir) :
|
|
772
|
+
path.join(repoRoot.root, 'autogenerated', normalizedVersion, 'rpk');
|
|
773
|
+
|
|
774
|
+
if (!fs.existsSync(rpkDir)) {
|
|
775
|
+
return {
|
|
776
|
+
success: false,
|
|
777
|
+
error: `RPK docs directory not found at ${rpkDir}`,
|
|
778
|
+
suggestion: 'Generate RPK docs first using generate_rpk_docs tool, or specify custom path with rpk_dir parameter'
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
filePath = rpkDir;
|
|
782
|
+
|
|
783
|
+
// Sample 3 command files for review (hybrid approach)
|
|
784
|
+
const rpkFiles = fs.readdirSync(rpkDir).filter(f => f.endsWith('.adoc'));
|
|
785
|
+
if (rpkFiles.length > 0) {
|
|
786
|
+
const samplesToRead = Math.min(3, rpkFiles.length);
|
|
787
|
+
const samples = [];
|
|
788
|
+
for (let i = 0; i < samplesToRead; i++) {
|
|
789
|
+
const sampleFile = path.join(rpkDir, rpkFiles[i]);
|
|
790
|
+
const content = fs.readFileSync(sampleFile, 'utf8');
|
|
791
|
+
samples.push(`\n// File: ${rpkFiles[i]}\n${content}`);
|
|
792
|
+
}
|
|
793
|
+
contentSample = samples.join('\n\n---\n\n');
|
|
794
|
+
contentFilePath = rpkDir;
|
|
795
|
+
contentTotalLines = rpkFiles.length; // Total number of command files
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
filesAnalyzed++;
|
|
800
|
+
suggestions.push(`${doc_type} documentation generated successfully`);
|
|
801
|
+
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
default:
|
|
806
|
+
return {
|
|
807
|
+
success: false,
|
|
808
|
+
error: `Unknown doc_type: ${doc_type}`,
|
|
809
|
+
suggestion: 'Use one of: properties, metrics, rpk, rpcn_connectors'
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Ensure quality score doesn't go below 0
|
|
814
|
+
qualityScore = Math.max(0, qualityScore);
|
|
815
|
+
|
|
816
|
+
const results = {
|
|
817
|
+
success: true,
|
|
818
|
+
doc_type,
|
|
819
|
+
version: version || 'N/A',
|
|
820
|
+
files_analyzed: filesAnalyzed,
|
|
821
|
+
issues,
|
|
822
|
+
quality_score: qualityScore,
|
|
823
|
+
suggestions,
|
|
824
|
+
summary: `Reviewed ${doc_type} documentation. Quality score: ${qualityScore}/100. Found ${issues.length} issues.`,
|
|
825
|
+
style_guide_loaded: styleGuide !== null,
|
|
826
|
+
style_guide: styleGuide,
|
|
827
|
+
// Hybrid review approach: sample for quick check, full content path if deeper review needed
|
|
828
|
+
content_sample: contentSample || null,
|
|
829
|
+
content_file_path: contentFilePath,
|
|
830
|
+
content_total_lines: contentTotalLines || null,
|
|
831
|
+
content_sample_info: contentSample ?
|
|
832
|
+
(doc_type === 'rpk' ?
|
|
833
|
+
`Sample includes ${Math.min(3, contentTotalLines)} of ${contentTotalLines} command files` :
|
|
834
|
+
`Sample includes first 300 of ${contentTotalLines} lines`) : null,
|
|
835
|
+
// Instructions for LLM-based review
|
|
836
|
+
review_instructions: styleGuide && contentSample ? buildStyleReviewInstructions(doc_type) : null,
|
|
837
|
+
additional_review_needed: styleGuide && contentSample ?
|
|
838
|
+
'IMPORTANT: HYBRID REVIEW APPROACH\n\n' +
|
|
839
|
+
'1. First, review the content sample below against the style guide:\n' +
|
|
840
|
+
' - Terminology compliance (correct capitalization, approved terms)\n' +
|
|
841
|
+
' - Voice and tone (active voice, present tense, second person)\n' +
|
|
842
|
+
' - Formatting (heading capitalization, bold usage, em dashes)\n' +
|
|
843
|
+
' - Clarity and readability\n\n' +
|
|
844
|
+
'2. If you find style issues in the sample:\n' +
|
|
845
|
+
' - Report the issues found in the sample\n' +
|
|
846
|
+
' - Use the Read tool on content_file_path to review more content\n' +
|
|
847
|
+
' - Look for patterns that indicate systematic problems\n\n' +
|
|
848
|
+
'3. If no issues found in sample, the docs are likely good throughout.' :
|
|
849
|
+
styleGuide ?
|
|
850
|
+
'IMPORTANT: The programmatic review above checks for technical quality issues (missing descriptions, invalid xrefs, etc.). You should ALSO review the generated content against the style guide below for terminology, tone, and formatting compliance.' :
|
|
851
|
+
'Note: Style guide not available for additional review.'
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// Generate AsciiDoc report if requested
|
|
855
|
+
if (generate_report) {
|
|
856
|
+
// Sanitize version for filename if provided
|
|
857
|
+
let safeVersion = '';
|
|
858
|
+
if (version) {
|
|
859
|
+
try {
|
|
860
|
+
safeVersion = `-${sanitizeVersion(version)}`;
|
|
861
|
+
} catch (err) {
|
|
862
|
+
// If version sanitization fails, skip it in filename
|
|
863
|
+
safeVersion = '';
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const reportFilename = `review-${doc_type}${safeVersion}-${formatDate()}.adoc`;
|
|
867
|
+
const reportPath = path.join(repoRoot.root, reportFilename);
|
|
868
|
+
generateReviewReport(results, reportPath);
|
|
869
|
+
results.report_path = reportPath;
|
|
870
|
+
results.report_generated = true;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return results;
|
|
874
|
+
|
|
875
|
+
} catch (err) {
|
|
876
|
+
return {
|
|
877
|
+
success: false,
|
|
878
|
+
error: err.message,
|
|
879
|
+
suggestion: 'Check that the documentation has been generated and files exist'
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
module.exports = {
|
|
885
|
+
generateReviewReport,
|
|
886
|
+
reviewGeneratedDocs
|
|
887
|
+
};
|