@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.
Files changed (61) hide show
  1. package/README.adoc +33 -1064
  2. package/bin/doc-tools-mcp.js +720 -0
  3. package/bin/doc-tools.js +1050 -50
  4. package/bin/mcp-tools/antora.js +153 -0
  5. package/bin/mcp-tools/cache.js +89 -0
  6. package/bin/mcp-tools/cloud-regions.js +127 -0
  7. package/bin/mcp-tools/content-review.js +196 -0
  8. package/bin/mcp-tools/crd-docs.js +153 -0
  9. package/bin/mcp-tools/frontmatter.js +138 -0
  10. package/bin/mcp-tools/generated-docs-review.js +887 -0
  11. package/bin/mcp-tools/helm-docs.js +152 -0
  12. package/bin/mcp-tools/index.js +245 -0
  13. package/bin/mcp-tools/job-queue.js +468 -0
  14. package/bin/mcp-tools/mcp-validation.js +266 -0
  15. package/bin/mcp-tools/metrics-docs.js +146 -0
  16. package/bin/mcp-tools/openapi.js +174 -0
  17. package/bin/mcp-tools/prompt-discovery.js +283 -0
  18. package/bin/mcp-tools/property-docs.js +157 -0
  19. package/bin/mcp-tools/rpcn-docs.js +113 -0
  20. package/bin/mcp-tools/rpk-docs.js +141 -0
  21. package/bin/mcp-tools/telemetry.js +211 -0
  22. package/bin/mcp-tools/utils.js +131 -0
  23. package/bin/mcp-tools/versions.js +168 -0
  24. package/cli-utils/convert-doc-links.js +1 -1
  25. package/cli-utils/github-token.js +58 -0
  26. package/cli-utils/self-managed-docs-branch.js +2 -2
  27. package/cli-utils/setup-mcp.js +313 -0
  28. package/docker-compose/25.1/transactions.md +1 -1
  29. package/docker-compose/transactions.md +1 -1
  30. package/extensions/DEVELOPMENT.adoc +464 -0
  31. package/extensions/README.adoc +124 -0
  32. package/extensions/REFERENCE.adoc +768 -0
  33. package/extensions/USER_GUIDE.adoc +339 -0
  34. package/extensions/generate-rp-connect-info.js +3 -4
  35. package/extensions/version-fetcher/get-latest-console-version.js +38 -27
  36. package/extensions/version-fetcher/get-latest-redpanda-version.js +65 -54
  37. package/extensions/version-fetcher/retry-util.js +88 -0
  38. package/extensions/version-fetcher/set-latest-version.js +6 -3
  39. package/macros/DEVELOPMENT.adoc +377 -0
  40. package/macros/README.adoc +105 -0
  41. package/macros/REFERENCE.adoc +222 -0
  42. package/macros/USER_GUIDE.adoc +220 -0
  43. package/macros/rp-connect-components.js +6 -6
  44. package/package.json +12 -3
  45. package/tools/bundle-openapi.js +20 -10
  46. package/tools/cloud-regions/generate-cloud-regions.js +1 -1
  47. package/tools/fetch-from-github.js +18 -4
  48. package/tools/gen-rpk-ascii.py +3 -1
  49. package/tools/generate-cli-docs.js +325 -0
  50. package/tools/get-console-version.js +4 -2
  51. package/tools/get-redpanda-version.js +4 -2
  52. package/tools/metrics/metrics.py +19 -7
  53. package/tools/property-extractor/Makefile +7 -1
  54. package/tools/property-extractor/cloud_config.py +4 -4
  55. package/tools/property-extractor/constant_resolver.py +11 -11
  56. package/tools/property-extractor/property_extractor.py +18 -16
  57. package/tools/property-extractor/topic_property_extractor.py +2 -2
  58. package/tools/property-extractor/transformers.py +7 -7
  59. package/tools/property-extractor/type_definition_extractor.py +4 -4
  60. package/tools/redpanda-connect/README.adoc +1 -1
  61. 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
+ };