@redpanda-data/docs-extensions-and-macros 4.13.1 → 4.13.3

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 (37) hide show
  1. package/bin/doc-tools-mcp.js +16 -4
  2. package/bin/doc-tools.js +768 -2089
  3. package/bin/mcp-tools/generated-docs-review.js +2 -2
  4. package/bin/mcp-tools/mcp-validation.js +1 -1
  5. package/bin/mcp-tools/openapi.js +2 -2
  6. package/bin/mcp-tools/property-docs.js +18 -0
  7. package/bin/mcp-tools/rpcn-docs.js +28 -3
  8. package/cli-utils/antora-utils.js +53 -2
  9. package/cli-utils/dependencies.js +313 -0
  10. package/cli-utils/diff-utils.js +273 -0
  11. package/cli-utils/doc-tools-utils.js +54 -0
  12. package/extensions/algolia-indexer/generate-index.js +134 -102
  13. package/extensions/algolia-indexer/index.js +70 -38
  14. package/extensions/collect-bloblang-samples.js +2 -1
  15. package/extensions/generate-rp-connect-categories.js +125 -67
  16. package/extensions/generate-rp-connect-info.js +291 -137
  17. package/macros/rp-connect-components.js +34 -5
  18. package/package.json +4 -3
  19. package/tools/add-commercial-names.js +207 -0
  20. package/tools/bundle-openapi.js +1 -1
  21. package/tools/generate-cli-docs.js +6 -2
  22. package/tools/get-console-version.js +5 -0
  23. package/tools/get-redpanda-version.js +5 -0
  24. package/tools/property-extractor/compare-properties.js +3 -3
  25. package/tools/property-extractor/generate-handlebars-docs.js +14 -14
  26. package/tools/property-extractor/generate-pr-summary.js +46 -0
  27. package/tools/property-extractor/pr-summary-formatter.js +375 -0
  28. package/tools/redpanda-connect/README.adoc +403 -38
  29. package/tools/redpanda-connect/connector-binary-analyzer.js +588 -0
  30. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +97 -34
  31. package/tools/redpanda-connect/parse-csv-connectors.js +1 -1
  32. package/tools/redpanda-connect/pr-summary-formatter.js +663 -0
  33. package/tools/redpanda-connect/report-delta.js +70 -2
  34. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +1279 -0
  35. package/tools/redpanda-connect/templates/connector.hbs +38 -0
  36. package/tools/redpanda-connect/templates/intro.hbs +0 -20
  37. package/tools/redpanda-connect/update-nav.js +216 -0
@@ -0,0 +1,663 @@
1
+ /**
2
+ * Format diff and cloud support data into a PR-friendly summary
3
+ * Outputs a console-parseable format for GitHub Actions
4
+ */
5
+
6
+ /**
7
+ * Generate a PR-friendly summary for connector changes
8
+ * @param {object} diffData - Diff data from generateConnectorDiffJson
9
+ * @param {object} binaryAnalysis - Cloud support data from getCloudSupport
10
+ * @param {array} draftedConnectors - Array of newly drafted connectors
11
+ * @returns {string} Formatted summary
12
+ */
13
+ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors = null) {
14
+ const lines = [];
15
+
16
+ // Header with delimiters for GitHub Action parsing
17
+ lines.push('<!-- PR_SUMMARY_START -->');
18
+ lines.push('');
19
+
20
+ // Quick Summary Section
21
+ lines.push('## 📊 Redpanda Connect Documentation Update');
22
+ lines.push('');
23
+ lines.push(`**OSS Version:** ${diffData.comparison.oldVersion} → ${diffData.comparison.newVersion}`);
24
+
25
+ if (binaryAnalysis) {
26
+ lines.push(`**Cloud Version:** ${binaryAnalysis.cloudVersion}`);
27
+ }
28
+
29
+ lines.push('');
30
+
31
+ // High-level stats
32
+ const stats = diffData.summary;
33
+ const hasChanges = Object.values(stats).some(v => v > 0) || (draftedConnectors && draftedConnectors.length > 0);
34
+
35
+ if (!hasChanges) {
36
+ lines.push('✅ **No changes detected** - Documentation is up to date');
37
+ lines.push('');
38
+ lines.push('<!-- PR_SUMMARY_END -->');
39
+ return lines.join('\n');
40
+ }
41
+
42
+ lines.push('### Summary');
43
+ lines.push('');
44
+
45
+ if (stats.newComponents > 0) {
46
+ lines.push(`- **${stats.newComponents}** new connector${stats.newComponents !== 1 ? 's' : ''}`);
47
+
48
+ if (binaryAnalysis) {
49
+ const newConnectorKeys = diffData.details.newComponents.map(c => `${c.type}:${c.name}`);
50
+ const cloudSupported = newConnectorKeys.filter(key => {
51
+ const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === key);
52
+ return inCloud;
53
+ }).length;
54
+
55
+ const needsCloudDocs = cloudSupported;
56
+
57
+ if (needsCloudDocs > 0) {
58
+ lines.push(` - ${needsCloudDocs} need${needsCloudDocs !== 1 ? '' : 's'} cloud docs ☁️`);
59
+ }
60
+ }
61
+ }
62
+
63
+ if (stats.newFields > 0) {
64
+ const affectedComponents = new Set(diffData.details.newFields.map(f => f.component)).size;
65
+ lines.push(`- **${stats.newFields}** new field${stats.newFields !== 1 ? 's' : ''} across ${affectedComponents} connector${affectedComponents !== 1 ? 's' : ''}`);
66
+ }
67
+
68
+ if (stats.removedComponents > 0) {
69
+ lines.push(`- **${stats.removedComponents}** removed connector${stats.removedComponents !== 1 ? 's' : ''} ⚠️`);
70
+ }
71
+
72
+ if (stats.removedFields > 0) {
73
+ lines.push(`- **${stats.removedFields}** removed field${stats.removedFields !== 1 ? 's' : ''} ⚠️`);
74
+ }
75
+
76
+ if (stats.deprecatedComponents > 0) {
77
+ lines.push(`- **${stats.deprecatedComponents}** deprecated connector${stats.deprecatedComponents !== 1 ? 's' : ''}`);
78
+ }
79
+
80
+ if (stats.deprecatedFields > 0) {
81
+ lines.push(`- **${stats.deprecatedFields}** deprecated field${stats.deprecatedFields !== 1 ? 's' : ''}`);
82
+ }
83
+
84
+ if (stats.changedDefaults > 0) {
85
+ lines.push(`- **${stats.changedDefaults}** default value change${stats.changedDefaults !== 1 ? 's' : ''} ⚠️`);
86
+ }
87
+
88
+ lines.push('');
89
+
90
+ // Writer Reminder for Commercial Names
91
+ if (stats.newComponents > 0) {
92
+ lines.push('### ✍️ Writer Action Required');
93
+ lines.push('');
94
+ lines.push('For each new connector, add the `:page-commercial-names:` attribute to the frontmatter:');
95
+ lines.push('');
96
+ lines.push('```asciidoc');
97
+ lines.push('= Connector Name');
98
+ lines.push(':type: input');
99
+ lines.push(':page-commercial-names: Commercial Name, Alternative Name');
100
+ lines.push('```');
101
+ lines.push('');
102
+ lines.push('_This helps improve discoverability and ensures proper categorization._');
103
+ lines.push('');
104
+
105
+ // Check if any new connectors are cloud-supported
106
+ if (binaryAnalysis && binaryAnalysis.comparison) {
107
+ const newConnectorKeys = diffData.details.newComponents.map(c => ({
108
+ key: `${c.type}:${c.name}`,
109
+ type: c.type,
110
+ name: c.name
111
+ }));
112
+
113
+ const cloudSupported = newConnectorKeys.filter(item => {
114
+ // Check both inCloud (OSS+Cloud) and cloudOnly (Cloud-only)
115
+ const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === item.key);
116
+ const cloudOnly = binaryAnalysis.comparison.cloudOnly &&
117
+ binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key);
118
+ return inCloud || cloudOnly;
119
+ });
120
+
121
+ if (cloudSupported.length > 0) {
122
+ lines.push('### ☁️ Cloud Docs Update Required');
123
+ lines.push('');
124
+ lines.push(`**${cloudSupported.length}** new connector${cloudSupported.length !== 1 ? 's are' : ' is'} available in Redpanda Cloud.`);
125
+ lines.push('');
126
+ lines.push('**Action:** Submit a separate PR to https://github.com/redpanda-data/cloud-docs to add the connector pages using include syntax:');
127
+ lines.push('');
128
+
129
+ // Check if any are cloud-only (need partial syntax)
130
+ const cloudOnly = cloudSupported.filter(item =>
131
+ binaryAnalysis.comparison.cloudOnly &&
132
+ binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key)
133
+ );
134
+ const regularCloud = cloudSupported.filter(item =>
135
+ !binaryAnalysis.comparison.cloudOnly ||
136
+ !binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key)
137
+ );
138
+
139
+ if (regularCloud.length > 0) {
140
+ lines.push('**For connectors in pages:**');
141
+ lines.push('```asciidoc');
142
+ lines.push('= Connector Name');
143
+ lines.push('');
144
+ lines.push('include::redpanda-connect:components:page$type/connector-name.adoc[tag=single-source]');
145
+ lines.push('```');
146
+ lines.push('');
147
+ }
148
+
149
+ if (cloudOnly.length > 0) {
150
+ lines.push('**For cloud-only connectors (in partials):**');
151
+ lines.push('```asciidoc');
152
+ lines.push('= Connector Name');
153
+ lines.push('');
154
+ lines.push('include::redpanda-connect:components:partial$components/cloud-only/type/connector-name.adoc[tag=single-source]');
155
+ lines.push('```');
156
+ lines.push('');
157
+ }
158
+
159
+ // Add instruction to update cloud whats-new
160
+ lines.push('**Also update the cloud whats-new file:**');
161
+ lines.push('');
162
+ lines.push('Add entries for the new cloud-supported connectors to the Redpanda Cloud whats-new file in the cloud-docs repository.');
163
+ lines.push('');
164
+ }
165
+ }
166
+ }
167
+
168
+ // Breaking Changes Section
169
+ const breakingChanges = [];
170
+ if (stats.removedComponents > 0) breakingChanges.push('removed connectors');
171
+ if (stats.removedFields > 0) breakingChanges.push('removed fields');
172
+ if (stats.changedDefaults > 0) breakingChanges.push('changed defaults');
173
+
174
+ if (breakingChanges.length > 0) {
175
+ lines.push('### ⚠️ Breaking Changes Detected');
176
+ lines.push('');
177
+ lines.push(`This update includes **${breakingChanges.join(', ')}** that may affect existing configurations.`);
178
+ lines.push('');
179
+ }
180
+
181
+ // Newly Drafted Connectors Section
182
+ if (draftedConnectors && draftedConnectors.length > 0) {
183
+ lines.push('### 📝 Newly Drafted - Needs Review');
184
+ lines.push('');
185
+ lines.push(`**${draftedConnectors.length}** connector${draftedConnectors.length !== 1 ? 's have' : ' has'} been auto-generated and placed in the proper location. These drafts need writer review:`);
186
+ lines.push('');
187
+
188
+ // Group by type
189
+ const draftsByType = {};
190
+ draftedConnectors.forEach(draft => {
191
+ const type = draft.type || 'unknown';
192
+ if (!draftsByType[type]) {
193
+ draftsByType[type] = [];
194
+ }
195
+ draftsByType[type].push(draft);
196
+ });
197
+
198
+ // List drafts by type
199
+ Object.entries(draftsByType).forEach(([type, drafts]) => {
200
+ lines.push(`**${type}:**`);
201
+ drafts.forEach(draft => {
202
+ const cloudIndicator = binaryAnalysis?.comparison.inCloud.some(c =>
203
+ c.type === type && c.name === draft.name
204
+ ) ? ' ☁️' : '';
205
+ const cgoIndicator = draft.requiresCgo ? ' 🔧' : '';
206
+ const statusBadge = draft.status && draft.status !== 'stable' ? ` (${draft.status})` : '';
207
+ lines.push(`- \`${draft.name}\`${statusBadge}${cloudIndicator}${cgoIndicator} → \`${draft.path}\``);
208
+ });
209
+ lines.push('');
210
+ });
211
+ }
212
+
213
+ // Missing Descriptions Warning
214
+ const missingDescriptions = [];
215
+
216
+ // Check for new components with missing descriptions
217
+ if (stats.newComponents > 0) {
218
+ diffData.details.newComponents.forEach(connector => {
219
+ if (!connector.description || connector.description.trim() === '') {
220
+ missingDescriptions.push({
221
+ type: 'component',
222
+ name: connector.name,
223
+ componentType: connector.type
224
+ });
225
+ }
226
+ });
227
+ }
228
+
229
+ // Check for new fields with missing descriptions
230
+ if (stats.newFields > 0) {
231
+ diffData.details.newFields.forEach(field => {
232
+ if (!field.description || field.description.trim() === '') {
233
+ missingDescriptions.push({
234
+ type: 'field',
235
+ name: field.field,
236
+ component: field.component
237
+ });
238
+ }
239
+ });
240
+ }
241
+
242
+ if (missingDescriptions.length > 0) {
243
+ lines.push('### ⚠️ Missing Descriptions');
244
+ lines.push('');
245
+ lines.push(`**${missingDescriptions.length}** item${missingDescriptions.length !== 1 ? 's' : ''} missing descriptions - these need writer attention:`);
246
+ lines.push('');
247
+
248
+ const componentsMissing = missingDescriptions.filter(m => m.type === 'component');
249
+ const fieldsMissing = missingDescriptions.filter(m => m.type === 'field');
250
+
251
+ if (componentsMissing.length > 0) {
252
+ lines.push('**Components:**');
253
+ componentsMissing.forEach(m => {
254
+ lines.push(`- \`${m.name}\` (${m.componentType})`);
255
+ });
256
+ lines.push('');
257
+ }
258
+
259
+ if (fieldsMissing.length > 0) {
260
+ lines.push('**Fields:**');
261
+ // Group by component
262
+ const fieldsByComponent = {};
263
+ fieldsMissing.forEach(m => {
264
+ if (!fieldsByComponent[m.component]) {
265
+ fieldsByComponent[m.component] = [];
266
+ }
267
+ fieldsByComponent[m.component].push(m.name);
268
+ });
269
+
270
+ Object.entries(fieldsByComponent).forEach(([component, fields]) => {
271
+ const [type, name] = component.split(':');
272
+ lines.push(`- **${type}/${name}:** ${fields.map(f => `\`${f}\``).join(', ')}`);
273
+ });
274
+ lines.push('');
275
+ }
276
+ }
277
+
278
+ // Action Items
279
+ lines.push('### 📝 Action Items for Writers');
280
+ lines.push('');
281
+
282
+ const actionItems = [];
283
+
284
+ // Add action items for missing descriptions
285
+ if (missingDescriptions.length > 0) {
286
+ actionItems.push({
287
+ priority: 0,
288
+ text: `⚠️ Add descriptions for ${missingDescriptions.length} component${missingDescriptions.length !== 1 ? 's' : ''}/field${missingDescriptions.length !== 1 ? 's' : ''} (see Missing Descriptions section)`
289
+ });
290
+ }
291
+
292
+ // New connectors that need cloud docs
293
+ if (binaryAnalysis && stats.newComponents > 0) {
294
+ diffData.details.newComponents.forEach(connector => {
295
+ const key = `${connector.type}:${connector.name}`;
296
+ const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === key);
297
+
298
+ if (inCloud) {
299
+ actionItems.push({
300
+ priority: 1,
301
+ text: `Document new \`${connector.name}\` ${connector.type} (☁️ **CLOUD SUPPORTED**)`
302
+ });
303
+ }
304
+ });
305
+ }
306
+
307
+ // New connectors without cloud support
308
+ if (stats.newComponents > 0) {
309
+ diffData.details.newComponents.forEach(connector => {
310
+ const key = `${connector.type}:${connector.name}`;
311
+ const inCloud = binaryAnalysis?.comparison.inCloud.some(c => `${c.type}:${c.name}` === key);
312
+
313
+ if (!inCloud) {
314
+ actionItems.push({
315
+ priority: 2,
316
+ text: `Document new \`${connector.name}\` ${connector.type} (self-hosted only)`
317
+ });
318
+ }
319
+ });
320
+ }
321
+
322
+ // Deprecated connectors
323
+ if (stats.deprecatedComponents > 0) {
324
+ diffData.details.deprecatedComponents.forEach(connector => {
325
+ actionItems.push({
326
+ priority: 3,
327
+ text: `Update docs for deprecated \`${connector.name}\` ${connector.type}`
328
+ });
329
+ });
330
+ }
331
+
332
+ // Removed connectors
333
+ if (stats.removedComponents > 0) {
334
+ diffData.details.removedComponents.forEach(connector => {
335
+ actionItems.push({
336
+ priority: 3,
337
+ text: `Update migration guide for removed \`${connector.name}\` ${connector.type}`
338
+ });
339
+ });
340
+ }
341
+
342
+ // Changed defaults that may break configs
343
+ if (stats.changedDefaults > 0) {
344
+ actionItems.push({
345
+ priority: 4,
346
+ text: `Review ${stats.changedDefaults} default value change${stats.changedDefaults !== 1 ? 's' : ''} for breaking changes`
347
+ });
348
+ }
349
+
350
+ // Sort by priority and output
351
+ actionItems.sort((a, b) => a.priority - b.priority);
352
+
353
+ if (actionItems.length > 0) {
354
+ actionItems.forEach(item => {
355
+ lines.push(`- [ ] ${item.text}`);
356
+ });
357
+ } else {
358
+ lines.push('- [ ] Review generated documentation');
359
+ }
360
+
361
+ lines.push('');
362
+
363
+ // Detailed breakdown (expandable)
364
+ lines.push('<details>');
365
+ lines.push('<summary><strong>📋 Detailed Changes</strong> (click to expand)</summary>');
366
+ lines.push('');
367
+
368
+ // New Connectors
369
+ if (stats.newComponents > 0) {
370
+ lines.push('#### New Connectors');
371
+ lines.push('');
372
+
373
+ if (binaryAnalysis) {
374
+ const cloudSupportedNew = [];
375
+ const selfHostedOnlyNew = [];
376
+
377
+ diffData.details.newComponents.forEach(connector => {
378
+ const key = `${connector.type}:${connector.name}`;
379
+ const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === key);
380
+
381
+ const entry = {
382
+ name: connector.name,
383
+ type: connector.type,
384
+ status: connector.status,
385
+ description: connector.description
386
+ };
387
+
388
+ if (inCloud) {
389
+ cloudSupportedNew.push(entry);
390
+ } else {
391
+ selfHostedOnlyNew.push(entry);
392
+ }
393
+ });
394
+
395
+ if (cloudSupportedNew.length > 0) {
396
+ lines.push('**☁️ Cloud Supported:**');
397
+ lines.push('');
398
+ cloudSupportedNew.forEach(c => {
399
+ lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
400
+ if (c.description) {
401
+ const shortDesc = truncateToSentence(c.description, 2);
402
+ lines.push(` - ${shortDesc}`);
403
+ }
404
+ });
405
+ lines.push('');
406
+ }
407
+
408
+ if (selfHostedOnlyNew.length > 0) {
409
+ lines.push('**Self-Hosted Only:**');
410
+ lines.push('');
411
+ selfHostedOnlyNew.forEach(c => {
412
+ lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
413
+ if (c.description) {
414
+ const shortDesc = truncateToSentence(c.description, 2);
415
+ lines.push(` - ${shortDesc}`);
416
+ }
417
+ });
418
+ lines.push('');
419
+ }
420
+ } else {
421
+ // No cloud support info, just list all
422
+ diffData.details.newComponents.forEach(c => {
423
+ lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
424
+ if (c.description) {
425
+ const shortDesc = truncateToSentence(c.description, 2);
426
+ lines.push(` - ${shortDesc}`);
427
+ }
428
+ });
429
+ lines.push('');
430
+ }
431
+ }
432
+
433
+ // cgo-only connectors (if any new connectors require cgo)
434
+ if (binaryAnalysis && binaryAnalysis.cgoOnly && binaryAnalysis.cgoOnly.length > 0 && stats.newComponents > 0) {
435
+ // Find new connectors that are cgo-only
436
+ const newCgoConnectors = diffData.details.newComponents.filter(connector => {
437
+ const key = `${connector.type}:${connector.name}`;
438
+ return binaryAnalysis.cgoOnly.some(cgo => `${cgo.type}:${cgo.name}` === key);
439
+ });
440
+
441
+ if (newCgoConnectors.length > 0) {
442
+ lines.push('#### 🔧 Cgo Requirements');
443
+ lines.push('');
444
+ lines.push('The following new connectors require cgo-enabled builds:');
445
+ lines.push('');
446
+
447
+ newCgoConnectors.forEach(connector => {
448
+ // Convert type to singular form for better grammar (e.g., "inputs" -> "input")
449
+ const typeSingular = connector.type.endsWith('s') ? connector.type.slice(0, -1) : connector.type;
450
+
451
+ lines.push(`**${connector.name}** (${connector.type}):`);
452
+ lines.push('');
453
+ lines.push('[NOTE]');
454
+ lines.push('====');
455
+ lines.push(`The \`${connector.name}\` ${typeSingular} requires a cgo-enabled build of Redpanda Connect.`);
456
+ lines.push('');
457
+ lines.push('For instructions, see:');
458
+ lines.push('');
459
+ lines.push('* xref:install:prebuilt-binary.adoc[Download a cgo-enabled binary]');
460
+ lines.push('* xref:install:build-from-source.adoc[Build Redpanda Connect from source]');
461
+ lines.push('====');
462
+ lines.push('');
463
+ });
464
+ }
465
+ }
466
+
467
+ // New Fields
468
+ if (stats.newFields > 0) {
469
+ lines.push('#### New Fields');
470
+ lines.push('');
471
+
472
+ // Group by component
473
+ const fieldsByComponent = {};
474
+ diffData.details.newFields.forEach(field => {
475
+ if (!fieldsByComponent[field.component]) {
476
+ fieldsByComponent[field.component] = [];
477
+ }
478
+ fieldsByComponent[field.component].push(field);
479
+ });
480
+
481
+ Object.entries(fieldsByComponent).forEach(([component, fields]) => {
482
+ const [type, name] = component.split(':');
483
+ lines.push(`**${type}/${name}:**`);
484
+ fields.forEach(f => {
485
+ lines.push(`- \`${f.field}\`${f.introducedIn ? ` (since ${f.introducedIn})` : ''}`);
486
+ });
487
+ lines.push('');
488
+ });
489
+ }
490
+
491
+ // Removed Connectors
492
+ if (stats.removedComponents > 0) {
493
+ lines.push('#### ⚠️ Removed Connectors');
494
+ lines.push('');
495
+ diffData.details.removedComponents.forEach(c => {
496
+ lines.push(`- **${c.name}** (${c.type})`);
497
+ });
498
+ lines.push('');
499
+ }
500
+
501
+ // Removed Fields
502
+ if (stats.removedFields > 0) {
503
+ lines.push('#### ⚠️ Removed Fields');
504
+ lines.push('');
505
+
506
+ const fieldsByComponent = {};
507
+ diffData.details.removedFields.forEach(field => {
508
+ if (!fieldsByComponent[field.component]) {
509
+ fieldsByComponent[field.component] = [];
510
+ }
511
+ fieldsByComponent[field.component].push(field);
512
+ });
513
+
514
+ Object.entries(fieldsByComponent).forEach(([component, fields]) => {
515
+ const [type, name] = component.split(':');
516
+ lines.push(`**${type}/${name}:**`);
517
+ fields.forEach(f => {
518
+ lines.push(`- \`${f.field}\``);
519
+ });
520
+ lines.push('');
521
+ });
522
+ }
523
+
524
+ // Deprecated Connectors
525
+ if (stats.deprecatedComponents > 0) {
526
+ lines.push('#### Deprecated Connectors');
527
+ lines.push('');
528
+ diffData.details.deprecatedComponents.forEach(c => {
529
+ lines.push(`- **${c.name}** (${c.type})`);
530
+ });
531
+ lines.push('');
532
+ }
533
+
534
+ // Deprecated Fields
535
+ if (stats.deprecatedFields > 0) {
536
+ lines.push('#### Deprecated Fields');
537
+ lines.push('');
538
+
539
+ const fieldsByComponent = {};
540
+ diffData.details.deprecatedFields.forEach(field => {
541
+ if (!fieldsByComponent[field.component]) {
542
+ fieldsByComponent[field.component] = [];
543
+ }
544
+ fieldsByComponent[field.component].push(field);
545
+ });
546
+
547
+ Object.entries(fieldsByComponent).forEach(([component, fields]) => {
548
+ const [type, name] = component.split(':');
549
+ lines.push(`**${type}/${name}:**`);
550
+ fields.forEach(f => {
551
+ lines.push(`- \`${f.field}\``);
552
+ });
553
+ lines.push('');
554
+ });
555
+ }
556
+
557
+ // Changed Defaults
558
+ if (stats.changedDefaults > 0) {
559
+ lines.push('#### ⚠️ Changed Default Values');
560
+ lines.push('');
561
+
562
+ const changesByComponent = {};
563
+ diffData.details.changedDefaults.forEach(change => {
564
+ if (!changesByComponent[change.component]) {
565
+ changesByComponent[change.component] = [];
566
+ }
567
+ changesByComponent[change.component].push(change);
568
+ });
569
+
570
+ Object.entries(changesByComponent).forEach(([component, changes]) => {
571
+ const [type, name] = component.split(':');
572
+ lines.push(`**${type}/${name}:**`);
573
+ changes.forEach(c => {
574
+ const oldStr = JSON.stringify(c.oldDefault);
575
+ const newStr = JSON.stringify(c.newDefault);
576
+ lines.push(`- \`${c.field}\`: ${oldStr} → ${newStr}`);
577
+ });
578
+ lines.push('');
579
+ });
580
+ }
581
+
582
+ // Cloud Support Gap Analysis
583
+ if (binaryAnalysis && binaryAnalysis.comparison.notInCloud.length > 0) {
584
+ lines.push('#### 🔍 Cloud Support Gap Analysis');
585
+ lines.push('');
586
+ lines.push(`**${binaryAnalysis.comparison.notInCloud.length} connector${binaryAnalysis.comparison.notInCloud.length !== 1 ? 's' : ''} available in OSS but not in cloud:**`);
587
+ lines.push('');
588
+
589
+ // Group by type
590
+ const gapsByType = {};
591
+ binaryAnalysis.comparison.notInCloud.forEach(connector => {
592
+ if (!gapsByType[connector.type]) {
593
+ gapsByType[connector.type] = [];
594
+ }
595
+ gapsByType[connector.type].push(connector);
596
+ });
597
+
598
+ Object.entries(gapsByType).forEach(([type, connectors]) => {
599
+ lines.push(`**${type}:**`);
600
+ connectors.forEach(c => {
601
+ lines.push(`- ${c.name} (${c.status})`);
602
+ });
603
+ lines.push('');
604
+ });
605
+ }
606
+
607
+ lines.push('</details>');
608
+ lines.push('');
609
+
610
+ // Footer
611
+ lines.push('---');
612
+ lines.push('');
613
+ lines.push(`*Generated: ${diffData.comparison.timestamp}*`);
614
+ lines.push('');
615
+ lines.push('<!-- PR_SUMMARY_END -->');
616
+
617
+ return lines.join('\n');
618
+ }
619
+
620
+ /**
621
+ * Truncate description to specified number of sentences
622
+ * @param {string} text - Text to truncate
623
+ * @param {number} sentences - Number of sentences to keep
624
+ * @returns {string} Truncated text
625
+ */
626
+ function truncateToSentence(text, sentences = 2) {
627
+ if (!text) return '';
628
+
629
+ // Remove markdown formatting
630
+ let clean = text
631
+ .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Remove links
632
+ .replace(/[*_`]/g, '') // Remove emphasis
633
+ .replace(/\n/g, ' '); // Replace newlines with spaces
634
+
635
+ // Split by sentence boundaries
636
+ const sentenceRegex = /[^.!?]+[.!?]+/g;
637
+ const matches = clean.match(sentenceRegex);
638
+
639
+ if (!matches || matches.length === 0) {
640
+ return clean.substring(0, 150);
641
+ }
642
+
643
+ const truncated = matches.slice(0, sentences).join(' ').trim();
644
+
645
+ return truncated.length > 200 ? truncated.substring(0, 200) + '...' : truncated;
646
+ }
647
+
648
+ /**
649
+ * Print the PR summary to console
650
+ * @param {object} diffData - Diff data
651
+ * @param {object} binaryAnalysis - Cloud support data
652
+ * @param {array} draftedConnectors - Array of newly drafted connectors
653
+ */
654
+ function printPRSummary(diffData, binaryAnalysis = null, draftedConnectors = null) {
655
+ const summary = generatePRSummary(diffData, binaryAnalysis, draftedConnectors);
656
+ console.log('\n' + summary + '\n');
657
+ }
658
+
659
+ module.exports = {
660
+ generatePRSummary,
661
+ printPRSummary,
662
+ truncateToSentence
663
+ };