@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.55.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 (42) hide show
  1. package/bin/doc-tools-mcp.js +4 -5
  2. package/bin/doc-tools.js +39 -2
  3. package/bin/mcp-tools/generated-docs-review.js +26 -9
  4. package/bin/mcp-tools/overrides-audit.js +1 -1
  5. package/bin/mcp-tools/rpcn-docs.js +4 -14
  6. package/cli-utils/format-helm-spec.js +4 -3
  7. package/cli-utils/helm-commented-values.js +88 -18
  8. package/cli-utils/self-managed-docs-branch.js +36 -30
  9. package/extension-utils/html-utils.js +27 -0
  10. package/extensions/REFERENCE.adoc +20 -4
  11. package/extensions/add-git-dates.js +13 -1
  12. package/extensions/aggregate-terms.js +34 -20
  13. package/extensions/modify-connect-tag-playbook.js +181 -3
  14. package/extensions/resolve-xrefs-in-attachments.js +1 -12
  15. package/extensions/util/connect-docs-asset.js +6 -5
  16. package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
  17. package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
  18. package/extensions/version-fetcher/list-connect-releases.js +3 -3
  19. package/extensions/version-fetcher/release-lines.js +64 -0
  20. package/extensions/version-fetcher/set-latest-version.js +73 -3
  21. package/macros/README.adoc +1 -0
  22. package/macros/REFERENCE.adoc +99 -0
  23. package/macros/badge.js +5 -8
  24. package/macros/enterprise.js +2 -5
  25. package/macros/glossary.js +4 -14
  26. package/macros/iceberg-explorer.js +242 -0
  27. package/macros/prop.js +3 -13
  28. package/mcp/USER_GUIDE.adoc +25 -135
  29. package/package.json +4 -2
  30. package/tools/doc-impact-recording/index.js +190 -0
  31. package/tools/overrides-audit/README.adoc +1 -2
  32. package/tools/overrides-audit/adapters/properties.js +1 -1
  33. package/tools/overrides-audit/index.js +3 -4
  34. package/tools/redpanda-connect/AUTOMATION.md +3 -36
  35. package/tools/redpanda-connect/README.adoc +5 -33
  36. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +3 -3
  37. package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
  38. package/tools/redpanda-connect/report-delta.js +6 -3
  39. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +38 -16
  40. package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
  41. package/tools/rpk-docs/templates/command.hbs +1 -1
  42. package/tools/overrides-audit/adapters/connect.js +0 -144
@@ -179,7 +179,7 @@ function renderStyleRegressionWarnings(warnings) {
179
179
  if (!Array.isArray(warnings) || warnings.length === 0) return [];
180
180
  const lines = [];
181
181
  lines.push('> [!WARNING]');
182
- lines.push('> **This update reintroduces house-style issues that the published docs do not have.** The fix belongs upstream in the Connect source, or in `docs-data/overrides.json`: correcting the generated partial is undone by the next regeneration.');
182
+ lines.push('> **This update reintroduces house-style issues that the published docs do not have.** The fix belongs upstream in the connect or benthos component spec: correcting the generated partial is undone by the next regeneration.');
183
183
  for (const w of warnings) {
184
184
  lines.push(`> - \`${w.partial}\` adds: ${w.issues.join(', ')}`);
185
185
  }
@@ -323,7 +323,8 @@ function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, drafte
323
323
  lines.push('|-----------|-------|-------------|');
324
324
 
325
325
  newFields.forEach(field => {
326
- const desc = field.description ? truncateToSentence(field.description, 1).replace(/\|/g, '\\|') : '_No description_';
326
+ const text = field.shortDescription || field.description;
327
+ const desc = text ? truncateToSentence(text, 1).replace(/\|/g, '\\|') : '_No description_';
327
328
  lines.push(`| \`${field.component}\` | \`${field.field}\` | ${desc} |`);
328
329
  });
329
330
  lines.push('');
@@ -359,7 +360,8 @@ function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, drafte
359
360
  lines.push(`**Deprecated Fields (${summary.deprecatedFields}):**`);
360
361
  lines.push('');
361
362
  deprecatedFields.forEach(field => {
362
- const guidance = field.description ? ` — ${truncateToSentence(field.description, 1)}` : '';
363
+ const text = field.shortDescription || field.description;
364
+ const guidance = text ? ` — ${truncateToSentence(text, 1)}` : '';
363
365
  lines.push(`- \`${field.component}.${field.field}\`${guidance}`);
364
366
  });
365
367
  lines.push('');
@@ -436,7 +438,7 @@ function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, drafte
436
438
  allDeprecatedFields.push({
437
439
  component: field.component,
438
440
  field: field.field,
439
- description: field.description,
441
+ description: field.shortDescription || field.description,
440
442
  version: release.toVersion
441
443
  });
442
444
  });
@@ -77,7 +77,8 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
77
77
  // span being compared: inputs:oracledb_cdc/snapshot_mode reports
78
78
  // 4.99.0 in the 4.98.0 to 4.103.1 diff.
79
79
  introducedIn: (rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version)) || opts.newVersion || null,
80
- description: rawFieldObj && rawFieldObj.description
80
+ description: rawFieldObj && rawFieldObj.description,
81
+ shortDescription: rawFieldObj && rawFieldObj.short_description
81
82
  });
82
83
  });
83
84
  });
@@ -193,7 +194,8 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
193
194
  deprecatedFields.push({
194
195
  component: cKey,
195
196
  field: fName,
196
- description: newFieldObj && newFieldObj.description
197
+ description: newFieldObj && newFieldObj.description,
198
+ shortDescription: newFieldObj && newFieldObj.short_description
197
199
  });
198
200
  }
199
201
 
@@ -212,7 +214,8 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
212
214
  field: fName,
213
215
  oldDefault: oldDefault,
214
216
  newDefault: newDefault,
215
- description: newFieldObj && newFieldObj.description
217
+ description: newFieldObj && newFieldObj.description,
218
+ shortDescription: newFieldObj && newFieldObj.short_description
216
219
  });
217
220
  }
218
221
  }
@@ -47,6 +47,21 @@ function capToTwoSentences (description) {
47
47
  /\bNo\./gi
48
48
  ]
49
49
 
50
+ // A table cell takes one paragraph of plain text. Keep only the text before
51
+ // the first blank line or list item, so a description such as
52
+ // "Controls how X is sized:\n\n- `a`: ...\n- `b`: ..." does not leak half a
53
+ // list into the cell. A lead-in that ends with a colon is left unterminated
54
+ // so the sentence match below drops it whenever a complete sentence comes
55
+ // before it ("Configure TCP settings. These controls are useful for:" keeps
56
+ // only the first sentence). Only a lead-in with nothing before it is kept,
57
+ // with the colon turned into a period.
58
+ let colonLeadIn = false
59
+ const firstBlock = description.split(/\n\s*\n|\n(?=\s*(?:[-*]|\d+\.)\s)/)[0].trim()
60
+ if (firstBlock && firstBlock !== description.trim() && !/^(?:[=#]|```|----)/.test(firstBlock)) {
61
+ description = firstBlock
62
+ colonLeadIn = firstBlock.endsWith(':')
63
+ }
64
+
50
65
  let normalized = description
51
66
  const placeholders = []
52
67
 
@@ -65,16 +80,19 @@ function capToTwoSentences (description) {
65
80
  })
66
81
 
67
82
  const sentenceRegex = /[^.!?]+[.!?]+(?:\s|$)/g
68
- const sentences = normalized.match(sentenceRegex)
69
-
70
- // If an unprotected mid-token period prevented the leading text from
71
- // forming a complete match, merge that dropped prefix into the first
72
- // sentence rather than silently discarding it.
73
- if (sentences && sentences.length > 0) {
74
- const firstIndex = normalized.indexOf(sentences[0])
75
- if (firstIndex > 0) {
76
- sentences[0] = normalized.slice(0, firstIndex) + sentences[0]
77
- }
83
+
84
+ // An unprotected mid-token period (`fetch.max.partition.bytes`) stops a
85
+ // match, and the regex then resumes after it, so the text before that
86
+ // period never belongs to any match. Merge each such gap into the sentence
87
+ // that follows it rather than silently discarding it, otherwise the second
88
+ // "sentence" can be a tail such as "bytes`.". Text after the last match
89
+ // (an unterminated colon lead-in, for example) is still dropped.
90
+ let sentences = null
91
+ let lastEnd = 0
92
+ for (const m of normalized.matchAll(sentenceRegex)) {
93
+ if (!sentences) sentences = []
94
+ sentences.push(normalized.slice(lastEnd, m.index) + m[0])
95
+ lastEnd = m.index + m[0].length
78
96
  }
79
97
 
80
98
  if (!sentences || sentences.length === 0) {
@@ -82,7 +100,7 @@ function capToTwoSentences (description) {
82
100
  placeholders.forEach(({ placeholder, original }) => {
83
101
  result = result.replace(placeholder, original)
84
102
  })
85
- return result
103
+ return colonLeadIn ? result.replace(/:$/, '.') : result
86
104
  }
87
105
 
88
106
  let maxSentences = 2
@@ -592,7 +610,7 @@ function buildFieldsTable (fields, capFn, opts = {}) {
592
610
  if (!byField[key]) {
593
611
  byField[key] = {
594
612
  field: field.field,
595
- description: field.description,
613
+ description: field.shortDescription || field.description,
596
614
  introducedIn: field.introducedIn,
597
615
  components: []
598
616
  }
@@ -655,7 +673,7 @@ function buildChangedDefaultsTable (changedDefaults, capFn) {
655
673
  field: change.field,
656
674
  oldDefault: change.oldDefault,
657
675
  newDefault: change.newDefault,
658
- description: change.description,
676
+ description: change.shortDescription || change.description,
659
677
  components: []
660
678
  }
661
679
  }
@@ -667,11 +685,15 @@ function buildChangedDefaultsTable (changedDefaults, capFn) {
667
685
  section += '|Field |Old default |New default |Description |Affected components\n\n'
668
686
 
669
687
  for (const [, info] of Object.entries(byFieldAndDefaults)) {
688
+ // Values render as literal inline code, so interpolations like
689
+ // ${!uuid_v4()} and characters like * or _ show as typed.
670
690
  const formatDefault = (val) => {
671
691
  if (val === undefined || val === null) return 'none'
672
- if (typeof val === 'string') return val
673
- if (typeof val === 'number' || typeof val === 'boolean') return String(val)
674
- return JSON.stringify(val)
692
+ let text
693
+ if (typeof val === 'string') text = val === '' ? '""' : val
694
+ else if (typeof val === 'number' || typeof val === 'boolean') text = String(val)
695
+ else text = JSON.stringify(val)
696
+ return `\`+${text.replace(/\|/g, '\\|')}+\``
675
697
  }
676
698
 
677
699
  const oldVal = formatDefault(info.oldDefault)
@@ -131,6 +131,118 @@ function insertReleaseSection(pageContent, sectionText) {
131
131
  return appendAtEnd();
132
132
  }
133
133
 
134
+ /**
135
+ * Reorders the entries within each `=== category` of a curated section so that
136
+ * entries sharing an `Area::` label sit together. Areas are ordered
137
+ * ALPHABETICALLY (case-insensitive), so a given area sits in the same place on
138
+ * every release instead of moving with the source's merge order; entries keep
139
+ * their source order within an area, so the sort is stable. The label is
140
+ * repeated per entry (not merged into one heading), so the page format is
141
+ * unchanged — only the order of entries within a category changes. Running it on
142
+ * an already-clustered section is a no-op, so it is safe to apply on every
143
+ * release and on a backfill.
144
+ *
145
+ * Only the clean single-line `Area:: description` definition-list form is
146
+ * reordered. A category whose entries are not all of that form — a phase-1
147
+ * candidate's plain `*` bullets, or a multi-line entry — is left in its original
148
+ * order, so this is a safe no-op on anything it does not recognize. A curated
149
+ * category (one with at least one `Area::` entry) that bails logs a warning, since
150
+ * it then ships unclustered. Area keys are trimmed for grouping, so `Kafka API`
151
+ * and `Kafka API ` are one area. Headings and any prose outside a category are
152
+ * passed through untouched.
153
+ *
154
+ * @param {string} sectionText - A rendered release section.
155
+ * @return {string} The section with each category's entries clustered by area,
156
+ * ending in a single newline.
157
+ */
158
+ function clusterSectionByArea(sectionText) {
159
+ // The area label is the text up to the first `:: ` (double-colon + space),
160
+ // which is the AsciiDoc definition-list term separator. A lazy match stops at
161
+ // that first separator, so a `::` later in the description is left alone. A
162
+ // leading list marker (`*`, `-`, `.`), at column zero or indented, is rejected
163
+ // so a plain bullet that happens to carry a `::` — a phase-1 candidate line —
164
+ // is not mistaken for an area entry; its category then bails and is left
165
+ // untouched.
166
+ const ENTRY_RE = /^(?!\s*[*.\-]+\s)(.+?)::\s/;
167
+
168
+ // Tokenize into paragraphs: maximal runs of non-blank lines. In this format
169
+ // every heading and every entry is one such paragraph.
170
+ const lines = String(sectionText).replace(/\n+$/, '').split('\n');
171
+ const paragraphs = [];
172
+ let current = [];
173
+ for (const line of lines) {
174
+ if (/^\s*$/.test(line)) {
175
+ if (current.length) { paragraphs.push(current); current = []; }
176
+ } else {
177
+ current.push(line);
178
+ }
179
+ }
180
+ if (current.length) paragraphs.push(current);
181
+
182
+ const out = [];
183
+ let categoryEntries = null; // entry paragraphs of the open `=== category`
184
+ let categoryBail = false; // the open category has an unrecognized entry
185
+ let categoryTitle = ''; // the open category's heading text, for warnings
186
+
187
+ const flushCategory = () => {
188
+ if (!categoryEntries) return;
189
+ if (categoryBail) {
190
+ // A phase-1 bullet category is expected to bail. A curated one (it has at
191
+ // least one `Area::` entry) bailing means some entry spans several lines,
192
+ // so the whole category ships unclustered — say so rather than fail quietly.
193
+ if (categoryEntries.some((entry) => ENTRY_RE.test(entry[0]))) {
194
+ console.warn(`${LOG_TAG} WARN: "${categoryTitle}" not grouped by area: an entry is not a single-line \`Area:: description\` (for example a \`+\` continuation or a code block).`);
195
+ }
196
+ for (const entry of categoryEntries) out.push(entry);
197
+ } else {
198
+ const order = [];
199
+ const groups = new Map();
200
+ for (const entry of categoryEntries) {
201
+ // Trimmed, so a stray space before `::` does not split one area in two.
202
+ // Only the grouping key is trimmed; the entry text is emitted verbatim.
203
+ const key = entry[0].match(ENTRY_RE)[1].trim();
204
+ if (!groups.has(key)) { groups.set(key, []); order.push(key); }
205
+ groups.get(key).push(entry);
206
+ }
207
+ // Areas alphabetical (case-insensitive), so an area lands in the same
208
+ // place on every release; entries keep their source order within an area.
209
+ order.sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));
210
+ for (const key of order) {
211
+ for (const entry of groups.get(key)) out.push(entry);
212
+ }
213
+ }
214
+ categoryEntries = null;
215
+ categoryBail = false;
216
+ };
217
+
218
+ for (const para of paragraphs) {
219
+ const first = para[0];
220
+ if (/^===\s/.test(first)) {
221
+ // A category heading closes the previous category and opens a new one.
222
+ flushCategory();
223
+ out.push(para);
224
+ categoryEntries = [];
225
+ categoryBail = false;
226
+ categoryTitle = first.replace(/^===\s+/, '').trim();
227
+ } else if (/^==\s/.test(first)) {
228
+ // The release heading (or any other top-level heading) closes the open
229
+ // category; entries do not cross it.
230
+ flushCategory();
231
+ out.push(para);
232
+ categoryEntries = null;
233
+ } else if (categoryEntries) {
234
+ categoryEntries.push(para);
235
+ if (para.length !== 1 || !ENTRY_RE.test(para[0])) categoryBail = true;
236
+ } else {
237
+ // Prose outside any category (for example an intro paragraph): untouched.
238
+ out.push(para);
239
+ }
240
+ }
241
+ flushCategory();
242
+
243
+ return out.map((p) => p.join('\n')).join('\n\n') + '\n';
244
+ }
245
+
134
246
  /**
135
247
  * Builds a candidate release section from a raw release body.
136
248
  *
@@ -221,7 +333,9 @@ function generateReleaseNotes({ body, section, tag, date, pageContent }) {
221
333
  let finalSection;
222
334
  if (section != null) {
223
335
  assertSectionMatchesTag(section, version);
224
- finalSection = section;
336
+ // Deterministically cluster entries by area so the curation step does not
337
+ // have to, and so a hand-written section is normalized the same way.
338
+ finalSection = clusterSectionByArea(section);
225
339
  } else {
226
340
  finalSection = buildReleaseSection({ body, version, date });
227
341
  }
@@ -232,6 +346,7 @@ function generateReleaseNotes({ body, section, tag, date, pageContent }) {
232
346
  module.exports = {
233
347
  generateReleaseNotes,
234
348
  buildReleaseSection,
349
+ clusterSectionByArea,
235
350
  assertSectionMatchesTag,
236
351
  insertReleaseSection,
237
352
  compareVersions,
@@ -86,7 +86,7 @@ include::{{{this}}}[]
86
86
  ifndef::env-cloud[]
87
87
  {{{description}}}
88
88
  endif::[]
89
- {{else if (eq descriptionScope "cloud-only")}}
89
+ {{else if (eq descriptionScope "cloud")}}
90
90
  ifdef::env-cloud[]
91
91
  {{{description}}}
92
92
  endif::[]
@@ -1,144 +0,0 @@
1
- /**
2
- * Overrides Audit - Redpanda Connect Adapter (structural)
3
- *
4
- * Reads the rp-connect-docs docs-data/overrides.json:
5
- * - top-level `definitions`: shared field descriptions referenced with
6
- * `{"$ref": "#/definitions/<name>"}`
7
- * - component sections (`inputs`, `outputs`, `processors`, `caches`,
8
- * `tracers`, `scanners`, `rate-limits`): ARRAYS of connector entries
9
- * `{ name, summary?, description?, config?: { children: [...] },
10
- * examples?, version?, ... }` where config children nest recursively
11
- * (`{ name, description?, children? , $ref? }`).
12
- *
13
- * The prose fields (summary, description, config field descriptions) map to
14
- * ConfigSpec strings in connect Go source (`.Summary()`, `.Description()`,
15
- * `NewXField(n).Description(...)`).
16
- *
17
- * TODO(quality logic): source comparison for connect is deferred. The
18
- * extracted side is the connect plugin JSON (rpk connect list --format json
19
- * or the cached connect-v<ver>.json in rp-connect-docs docs-data), and the
20
- * comparison must resolve $refs before classifying, then apply the same
21
- * markup rules as the properties surface (connect descriptions are AsciiDoc
22
- * inside Go strings, so xref:/glossterm: usage is docs-only there too).
23
- * Until then, prose fields classify REVIEW with an explicit note and
24
- * everything else classifies KEEP, so the manifest shape is stable.
25
- */
26
-
27
- 'use strict'
28
-
29
- const classify = require('../classify')
30
- const { loadJson } = require('./properties')
31
-
32
- const COMPONENT_SECTIONS = [
33
- 'inputs',
34
- 'outputs',
35
- 'processors',
36
- 'caches',
37
- 'tracers',
38
- 'scanners',
39
- 'rate-limits',
40
- 'buffers',
41
- 'metrics'
42
- ]
43
-
44
- const TODO_NOTE = 'TODO: connect source comparison not implemented yet (needs the connect plugin JSON and $ref resolution).'
45
-
46
- /**
47
- * Build a REVIEW row for a connect prose field.
48
- *
49
- * @param {string} name - Unit name (for example "inputs/amqp_0_9/urls").
50
- * @param {string} field - Manifest field label.
51
- * @param {string} text - Override text.
52
- * @returns {Object} Manifest row.
53
- */
54
- function proseRow (name, field, text) {
55
- return {
56
- name,
57
- field,
58
- class: classify.CLASSES.REVIEW,
59
- content_hash: classify.contentHash(name, text),
60
- note: TODO_NOTE
61
- }
62
- }
63
-
64
- /**
65
- * Walk a config children tree, emitting one row per field description.
66
- *
67
- * @param {string} prefix - Name prefix ("inputs/amqp_0_9").
68
- * @param {Object[]} children - Config children array.
69
- * @param {Object[]} manifest - Output rows (mutated).
70
- */
71
- function walkConfigChildren (prefix, children, manifest) {
72
- if (!Array.isArray(children)) return
73
- for (const child of children) {
74
- if (!child || typeof child !== 'object') continue
75
- const childName = `${prefix}/${child.name || '(unnamed)'}`
76
- if (typeof child.description === 'string') {
77
- manifest.push(proseRow(childName, 'config.description', child.description))
78
- }
79
- if (child.$ref) {
80
- manifest.push({
81
- name: childName,
82
- field: 'config.$ref',
83
- class: classify.CLASSES.KEEP,
84
- note: `Shared definition reference (${child.$ref}); audit the referenced definition instead.`
85
- })
86
- }
87
- if (child.children) walkConfigChildren(childName, child.children, manifest)
88
- }
89
- }
90
-
91
- /**
92
- * Run the audit for the connect surface (structural pass).
93
- *
94
- * @param {Object} args - { overridesPath, extractedPath }.
95
- * @returns {Object} { surface, manifest, summary }.
96
- */
97
- function audit ({ overridesPath, extractedPath }) {
98
- const overridesDoc = loadJson(overridesPath, 'connect overrides')
99
-
100
- const manifest = []
101
-
102
- const definitions = overridesDoc.definitions
103
- if (definitions && typeof definitions === 'object') {
104
- for (const [defName, def] of Object.entries(definitions)) {
105
- if (def && typeof def.description === 'string') {
106
- manifest.push(proseRow(`definitions/${defName}`, 'description', def.description))
107
- }
108
- }
109
- }
110
-
111
- for (const section of COMPONENT_SECTIONS) {
112
- const entries = overridesDoc[section]
113
- if (!Array.isArray(entries)) continue
114
- for (const entry of entries) {
115
- if (!entry || typeof entry !== 'object') continue
116
- const entryName = `${section}/${entry.name || '(unnamed)'}`
117
- for (const [field, value] of Object.entries(entry)) {
118
- if (field === 'name') continue
119
- if (field === 'summary' || field === 'description') {
120
- manifest.push(proseRow(entryName, field, value))
121
- } else if (field === 'config' && value && typeof value === 'object') {
122
- walkConfigChildren(entryName, value.children, manifest)
123
- } else {
124
- manifest.push({
125
- name: entryName,
126
- field,
127
- class: classify.CLASSES.KEEP,
128
- note: 'Docs-side connector metadata; stays in the override file by design.'
129
- })
130
- }
131
- }
132
- }
133
- }
134
-
135
- return {
136
- surface: 'connect',
137
- overrides_file: overridesPath,
138
- extracted_file: extractedPath || null,
139
- manifest,
140
- summary: classify.summarize(manifest)
141
- }
142
- }
143
-
144
- module.exports = { audit, COMPONENT_SECTIONS }