@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.56.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 (87) hide show
  1. package/bin/doc-tools-mcp.js +4 -5
  2. package/bin/doc-tools.js +354 -9
  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/docs-links-to-xrefs.js +174 -0
  7. package/cli-utils/format-helm-spec.js +4 -3
  8. package/cli-utils/generated-marker.js +43 -0
  9. package/cli-utils/helm-commented-values.js +125 -21
  10. package/cli-utils/self-managed-docs-branch.js +36 -30
  11. package/extension-utils/html-utils.js +27 -0
  12. package/extension-utils/llms-utils.js +6 -2
  13. package/extension-utils/metadata-candidates.js +304 -0
  14. package/extension-utils/metadata-check.js +174 -0
  15. package/extension-utils/metadata-suggest.js +573 -0
  16. package/extension-utils/taxonomy-categories.js +106 -0
  17. package/extension-utils/taxonomy.js +566 -0
  18. package/extensions/REFERENCE.adoc +110 -8
  19. package/extensions/add-git-dates.js +13 -1
  20. package/extensions/aggregate-terms.js +34 -20
  21. package/extensions/algolia-indexer/generate-index.js +13 -1
  22. package/extensions/algolia-indexer/index.js +4 -2
  23. package/extensions/convert-sitemap-to-markdown.js +47 -12
  24. package/extensions/convert-to-markdown.js +176 -0
  25. package/extensions/generate-fields-only-pages.js +14 -0
  26. package/extensions/modify-connect-tag-playbook.js +181 -3
  27. package/extensions/resolve-xrefs-in-attachments.js +1 -12
  28. package/extensions/url-to-xref.js +92 -22
  29. package/extensions/util/connect-docs-asset.js +6 -5
  30. package/extensions/validate-attributes.js +10 -1
  31. package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
  32. package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
  33. package/extensions/version-fetcher/list-connect-releases.js +3 -3
  34. package/extensions/version-fetcher/release-lines.js +64 -0
  35. package/extensions/version-fetcher/set-latest-version.js +73 -3
  36. package/macros/README.adoc +1 -0
  37. package/macros/REFERENCE.adoc +99 -0
  38. package/macros/badge.js +5 -8
  39. package/macros/enterprise.js +2 -5
  40. package/macros/glossary.js +4 -14
  41. package/macros/iceberg-explorer.js +242 -0
  42. package/macros/prop.js +3 -13
  43. package/mcp/USER_GUIDE.adoc +25 -135
  44. package/package.json +5 -2
  45. package/tools/check-metadata/index.js +523 -0
  46. package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
  47. package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
  48. package/tools/cloud-regions/render-cloud-regions.js +3 -0
  49. package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
  50. package/tools/dispatch-graph/classify-runs.js +128 -0
  51. package/tools/dispatch-graph/parse-workflows.js +499 -0
  52. package/tools/doc-impact-recording/index.js +190 -0
  53. package/tools/enterprise-features/detect-gating-changes.js +393 -0
  54. package/tools/enterprise-features/verify.js +2 -1
  55. package/tools/gen-rpk-ascii.py +7 -1
  56. package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
  57. package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
  58. package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
  59. package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
  60. package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
  61. package/tools/lint-strings/index.js +133 -3
  62. package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
  63. package/tools/lint-strings/surfaces/connect.js +72 -12
  64. package/tools/lint-strings/surfaces/crd.js +10 -6
  65. package/tools/metrics/metrics.py +11 -0
  66. package/tools/overrides-audit/README.adoc +1 -2
  67. package/tools/overrides-audit/adapters/properties.js +1 -1
  68. package/tools/overrides-audit/index.js +3 -4
  69. package/tools/property-extractor/generate-handlebars-docs.js +9 -1
  70. package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
  71. package/tools/redpanda-connect/AUTOMATION.md +3 -36
  72. package/tools/redpanda-connect/README.adoc +5 -33
  73. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +32 -11
  74. package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
  75. package/tools/redpanda-connect/report-delta.js +6 -3
  76. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +42 -25
  77. package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
  78. package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
  79. package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
  80. package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
  81. package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
  82. package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
  83. package/tools/rpk-docs/generate-rpk-docs.js +2 -0
  84. package/tools/rpk-docs/generate-x-env-partial.js +7 -4
  85. package/tools/rpk-docs/helpers/index.js +4 -1
  86. package/tools/rpk-docs/templates/command.hbs +2 -2
  87. package/tools/overrides-audit/adapters/connect.js +0 -144
@@ -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,
@@ -11,6 +11,8 @@ Object.entries(helpers).forEach(([name, fn]) => {
11
11
  handlebars.registerHelper(name, fn)
12
12
  }
13
13
  })
14
+ // The provenance marker command.hbs writes under the page header
15
+ handlebars.registerHelper('generatedMarker', require('../../cli-utils/generated-marker').generatedMarker)
14
16
 
15
17
  // Template paths
16
18
  const TEMPLATES_DIR = path.resolve(__dirname, './templates')
@@ -28,17 +28,20 @@ const fs = require('fs')
28
28
  const os = require('os')
29
29
  const path = require('path')
30
30
  const { spawnSync } = require('child_process')
31
+ const { generatedMarker } = require('../../cli-utils/generated-marker')
31
32
 
32
33
  // A healthy rpk exposes ~30 -X options. Refuse to overwrite the partial
33
34
  // with a suspiciously small table (for example, if -X list output ever
34
35
  // changes format and only partially parses).
35
36
  const MIN_OPTIONS = 20
36
37
 
38
+ // The shared marker, so convert-to-markdown reports the partial as generated.
37
39
  const GENERATED_BANNER = `// tag::generated[]
38
- // Generated from rpk's own -X option data. Do not edit manually:
39
- // regenerate with doc-tools generate rpk-docs (which rewrites this
40
- // file alongside the rpk command pages) or, for a targeted refresh,
41
- // doc-tools generate rpk-env-partial.
40
+ ${generatedMarker(
41
+ 'rpk-env-partial',
42
+ "rpk's own -X option data (rpk --print-tree)",
43
+ 'Regenerate with doc-tools generate rpk-docs, which rewrites this file alongside the rpk command pages, or, for a targeted refresh, doc-tools generate rpk-env-partial.'
44
+ )}
42
45
  `
43
46
 
44
47
  /**
@@ -426,7 +426,10 @@ function wrapDescriptionPassthrough(str) {
426
426
  if (!str) return ''
427
427
  // Check if contains backticks (inline code) or other formatting markers
428
428
  if (/`/.test(str)) {
429
- return `pass:q[${str}]`
429
+ // pass:q[] skips special-character substitution, so a placeholder such as
430
+ // <agent> would reach the HTML raw and be dropped as an unknown tag.
431
+ const escaped = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
432
+ return `pass:q[${escaped}]`
430
433
  }
431
434
  return str
432
435
  }
@@ -22,7 +22,7 @@
22
22
  :page-deprecated: true
23
23
  {{/if}}
24
24
 
25
- // This content is autogenerated. Do not edit manually. To customize content, see the writer's guide: https://github.com/redpanda-data/docs/blob/main/docs-data/RPK_OVERRIDES_GUIDE.adoc
25
+ {{{generatedMarker "rpk-docs" "the rpk command help in redpanda-data/streaming-enterprise" "To customize content, see the writer's guide: https://github.com/redpanda-data/docs/blob/main/docs-data/RPK_OVERRIDES_GUIDE.adoc"}}}
26
26
  {{#each pageAttributes}}
27
27
  :{{@key}}: {{this}}
28
28
  {{/each}}
@@ -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 }