@redpanda-data/docs-extensions-and-macros 5.14.0 → 5.22.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 (56) hide show
  1. package/bin/doc-tools-mcp.js +88 -0
  2. package/bin/doc-tools.js +334 -102
  3. package/bin/mcp-tools/index.js +20 -0
  4. package/bin/mcp-tools/lint-doc-strings.js +75 -0
  5. package/bin/mcp-tools/overrides-audit.js +100 -0
  6. package/bin/mcp-tools/preview-doc-string.js +71 -0
  7. package/cli-utils/diff-utils.js +77 -0
  8. package/cli-utils/helm-commented-values.js +172 -23
  9. package/cli-utils/validation-result.js +53 -0
  10. package/macros/REFERENCE.adoc +3 -2
  11. package/macros/prop.js +34 -6
  12. package/package.json +2 -1
  13. package/tools/generate-cli-docs.js +28 -67
  14. package/tools/lint-strings/diff.js +137 -0
  15. package/tools/lint-strings/engine.js +163 -0
  16. package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
  17. package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
  18. package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
  19. package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
  20. package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
  21. package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
  22. package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
  23. package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
  24. package/tools/lint-strings/go-source.js +327 -0
  25. package/tools/lint-strings/index.js +229 -0
  26. package/tools/lint-strings/rules/common.js +148 -0
  27. package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
  28. package/tools/lint-strings/source-text.js +41 -0
  29. package/tools/lint-strings/surfaces/connect.js +233 -0
  30. package/tools/lint-strings/surfaces/crd.js +239 -0
  31. package/tools/lint-strings/surfaces/helm.js +159 -0
  32. package/tools/lint-strings/surfaces/metrics.js +368 -0
  33. package/tools/lint-strings/surfaces/properties.js +216 -0
  34. package/tools/lint-strings/surfaces/rpk.js +377 -0
  35. package/tools/overrides-audit/README.adoc +152 -0
  36. package/tools/overrides-audit/adapters/connect.js +144 -0
  37. package/tools/overrides-audit/adapters/properties.js +117 -0
  38. package/tools/overrides-audit/adapters/rpk.js +192 -0
  39. package/tools/overrides-audit/classify.js +546 -0
  40. package/tools/overrides-audit/index.js +185 -0
  41. package/tools/preview-string/index.js +245 -0
  42. package/tools/property-extractor/Makefile +9 -4
  43. package/tools/property-extractor/README.adoc +17 -3
  44. package/tools/property-extractor/helpers/index.js +1 -2
  45. package/tools/property-extractor/helpers/seeAlsoView.js +76 -0
  46. package/tools/property-extractor/parser.py +26 -0
  47. package/tools/property-extractor/property_extractor.py +65 -6
  48. package/tools/property-extractor/templates/property.hbs +25 -37
  49. package/tools/property-extractor/templates/topic-property.hbs +25 -37
  50. package/tools/property-extractor/transformers.py +15 -2
  51. package/tools/property-extractor/validate-overrides.js +120 -0
  52. package/tools/rpk-docs/detect-platform-commands.js +8 -1
  53. package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
  54. package/tools/rpk-docs/validate-overrides.js +1 -45
  55. package/tools/property-extractor/helpers/allTopicsConditional.js +0 -19
  56. package/tools/property-extractor/helpers/parseRelatedTopic.js +0 -13
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Overrides Audit CLI
5
+ *
6
+ * Field-level classification of docs-side override entries against
7
+ * extracted source strings, powering the override-retirement loop
8
+ * (retirement: delete REDUNDANT fields on each release regeneration;
9
+ * upstreaming: draft source PRs from UPSTREAMABLE/SPLIT candidates).
10
+ *
11
+ * Usage (standalone):
12
+ * node tools/overrides-audit/index.js \
13
+ * --overrides docs-data/property-overrides.json \
14
+ * --extracted tools/property-extractor/gen/properties-output.json \
15
+ * [--surface properties|rpk|connect] [--format json|human] [--output <file>]
16
+ *
17
+ * Also exposed as `doc-tools overrides audit` and the `audit_overrides`
18
+ * MCP tool. See README.adoc in this directory for the classification rules
19
+ * and the upstream_ref policy.
20
+ */
21
+
22
+ 'use strict'
23
+
24
+ const fs = require('fs')
25
+ const path = require('path')
26
+
27
+ const ADAPTERS = {
28
+ properties: () => require('./adapters/properties'),
29
+ rpk: () => require('./adapters/rpk'),
30
+ connect: () => require('./adapters/connect')
31
+ }
32
+
33
+ /**
34
+ * Run the audit and return the result object.
35
+ *
36
+ * @param {Object} options - { overrides, extracted, surface, format, output }.
37
+ * @returns {Object} Audit result ({ surface, manifest, summary, ... }).
38
+ */
39
+ function runAudit (options) {
40
+ const surface = options.surface || 'properties'
41
+ const adapterFactory = ADAPTERS[surface]
42
+ if (!adapterFactory) {
43
+ throw new Error(`Unknown surface '${surface}'. Valid surfaces: ${Object.keys(ADAPTERS).join(', ')}`)
44
+ }
45
+ if (!options.overrides) {
46
+ throw new Error('Missing required option --overrides <path>')
47
+ }
48
+ let extractedPath = options.extracted ? path.resolve(options.extracted) : undefined
49
+ if (surface === 'properties' && !extractedPath) {
50
+ if (!options.repo) {
51
+ throw new Error('Provide --extracted <path> (raw extractor JSON, no overrides applied) or --repo <path> (a redpanda checkout to extract from)')
52
+ }
53
+ // Extract raw source strings ourselves. Never audit against a published
54
+ // redpanda-properties-<tag>.json: those already have overrides applied,
55
+ // which classifies every description REDUNDANT.
56
+ const fs = require('fs')
57
+ const os = require('os')
58
+ const { runExtractor } = require('../lint-strings/surfaces/properties')
59
+ const json = runExtractor(path.resolve(options.repo), (msg) => console.error(msg))
60
+ extractedPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'overrides-audit-')), 'extracted.json')
61
+ fs.writeFileSync(extractedPath, JSON.stringify(json))
62
+ }
63
+
64
+ const adapter = adapterFactory()
65
+ return adapter.audit({
66
+ overridesPath: path.resolve(options.overrides),
67
+ extractedPath
68
+ })
69
+ }
70
+
71
+ /**
72
+ * Render the audit result as a human-readable report.
73
+ *
74
+ * @param {Object} result - Audit result from runAudit().
75
+ * @returns {string} Multi-line report.
76
+ */
77
+ function formatHumanReport (result) {
78
+ const lines = []
79
+ const { summary } = result
80
+ lines.push('='.repeat(64))
81
+ lines.push(`Overrides audit — surface: ${result.surface}`)
82
+ lines.push(`Overrides: ${result.overrides_file}`)
83
+ lines.push(`Extracted: ${result.extracted_file || '(none)'}`)
84
+ lines.push('='.repeat(64))
85
+ lines.push('')
86
+ lines.push(`Classified ${summary.total} override field(s):`)
87
+ const order = [
88
+ 'REDUNDANT',
89
+ 'UPSTREAMABLE',
90
+ 'KEEP_UNTIL_UPSTREAMED',
91
+ 'UPSTREAMABLE_SLOT',
92
+ 'REDUNDANT_OR_UPSTREAMABLE',
93
+ 'KEEP',
94
+ 'REVIEW'
95
+ ]
96
+ for (const cls of order) {
97
+ if (summary.byClass[cls]) {
98
+ lines.push(` ${cls.padEnd(26)} ${summary.byClass[cls]}`)
99
+ }
100
+ }
101
+ lines.push('')
102
+ lines.push('Per field:')
103
+ for (const [field, byClass] of Object.entries(summary.byField)) {
104
+ const parts = Object.entries(byClass).map(([cls, count]) => `${cls}: ${count}`)
105
+ lines.push(` ${field.padEnd(20)} ${parts.join(', ')}`)
106
+ }
107
+
108
+ const sections = [
109
+ ['REDUNDANT', 'Retire in the next auto-docs PR (source already matches)'],
110
+ ['UPSTREAMABLE', 'Send upstream verbatim'],
111
+ ['UPSTREAMABLE_SLOT', 'Migrate to a source metadata slot'],
112
+ ['REDUNDANT_OR_UPSTREAMABLE', 'Needs a human ruling'],
113
+ ['REVIEW', 'Needs attention (possible source bug, typo, or missing property)']
114
+ ]
115
+ for (const [cls, heading] of sections) {
116
+ const rows = result.manifest.filter((row) => row.class === cls)
117
+ if (rows.length === 0) continue
118
+ lines.push('')
119
+ lines.push(`${cls} — ${heading} (${rows.length}):`)
120
+ for (const row of rows) {
121
+ lines.push(` • ${row.name} [${row.field}]${row.note ? ` — ${row.note}` : ''}`)
122
+ }
123
+ }
124
+
125
+ if (result.cross_check) {
126
+ lines.push('')
127
+ lines.push('Cross-check (compare-properties.js):')
128
+ lines.push(` descriptions still masked by overrides: ${result.cross_check.changedDescriptions}`)
129
+ lines.push(` empty source descriptions: ${result.cross_check.emptyDescriptions}`)
130
+ if (result.cross_check.violations.length > 0) {
131
+ lines.push(` CLASSIFIER BUG — raw-equal but not REDUNDANT: ${result.cross_check.violations.join(', ')}`)
132
+ } else {
133
+ lines.push(' consistency: OK (every raw-equal description classified REDUNDANT)')
134
+ }
135
+ }
136
+
137
+ lines.push('')
138
+ lines.push('='.repeat(64))
139
+ return lines.join('\n')
140
+ }
141
+
142
+ /**
143
+ * CLI entry point.
144
+ *
145
+ * @param {string[]} argv - process.argv.
146
+ */
147
+ function main (argv) {
148
+ const { Command } = require('commander')
149
+ const program = new Command()
150
+
151
+ program
152
+ .name('overrides-audit')
153
+ .description('Classify docs-side override fields against extracted source strings')
154
+ .requiredOption('--overrides <path>', 'Path to the overrides JSON file (for example docs-data/property-overrides.json)')
155
+ .option('--extracted <path>', 'Path to the extracted source JSON (property extractor raw output; required for the properties surface)')
156
+ .option('--surface <surface>', 'Override surface: properties, rpk, or connect', 'properties')
157
+ .option('--format <format>', 'Output format: json or human', 'json')
158
+ .option('--output <path>', 'Also write the JSON result to this file')
159
+ .action((options) => {
160
+ try {
161
+ const result = runAudit(options)
162
+ if (options.output) {
163
+ fs.mkdirSync(path.dirname(path.resolve(options.output)), { recursive: true })
164
+ fs.writeFileSync(path.resolve(options.output), JSON.stringify(result, null, 2) + '\n')
165
+ console.error(`JSON result written to ${options.output}`)
166
+ }
167
+ if (options.format === 'human') {
168
+ console.log(formatHumanReport(result))
169
+ } else {
170
+ console.log(JSON.stringify(result, null, 2))
171
+ }
172
+ } catch (err) {
173
+ console.error(`Error: ${err.message}`)
174
+ process.exit(1)
175
+ }
176
+ })
177
+
178
+ program.parse(argv)
179
+ }
180
+
181
+ if (require.main === module) {
182
+ main(process.argv)
183
+ }
184
+
185
+ module.exports = { runAudit, formatHumanReport }
@@ -0,0 +1,245 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ /**
7
+ * doc-tools preview-string: render ONE doc-string declaration from local
8
+ * engineering source to the final published snippet, so an engineer can see
9
+ * what their string becomes on docs.redpanda.com BEFORE it ships.
10
+ *
11
+ * - properties: runs the real Python extractor and the real Handlebars
12
+ * property template. With --overrides, renders a second "as shipped"
13
+ * pane and prints a MASKED-BY-OVERRIDE notice when a docs-repo override
14
+ * would replace the source string.
15
+ * - rpk: runs the string through the real formatDescription() transformer
16
+ * from tools/rpk-docs (the killer demo: an ALLCAPS line becomes a "==="
17
+ * heading before your eyes).
18
+ * - metrics/helm/crd/connect: renders the description in the surface's
19
+ * output shape with a one-line header.
20
+ *
21
+ * Extraction reuses the lint-strings surface modules, so preview and lint
22
+ * always agree on what the source declares.
23
+ */
24
+
25
+ const HR = '-'.repeat(72)
26
+
27
+ function pane (title, body) {
28
+ return `${HR}\n${title}\n${HR}\n${body}\n`
29
+ }
30
+
31
+ /**
32
+ * Render a preview for one declaration.
33
+ *
34
+ * @param {Object} options
35
+ * @param {string} options.repo - Path to the engineering checkout
36
+ * @param {string} options.surface - properties|rpk|metrics|helm|crd|connect
37
+ * @param {string} options.name - Declaration name (property name, rpk
38
+ * command or --flag, metric name, helm key path, crd json field, connect
39
+ * component/field name)
40
+ * @param {string} [options.overrides] - Overrides JSON path (properties only)
41
+ * @param {Function} [options.log]
42
+ * @returns {string} The rendered preview text
43
+ */
44
+ function previewString (options) {
45
+ const { repo, surface, name, overrides = null, log = (msg) => process.stderr.write(`${msg}\n`) } = options
46
+ if (!repo) throw new Error('preview-string requires --repo <path>')
47
+ if (!surface) throw new Error('preview-string requires --surface <name>')
48
+ if (!name) throw new Error('preview-string requires --name <name>')
49
+ const repoPath = path.resolve(repo)
50
+ if (!fs.existsSync(repoPath)) throw new Error(`Repo path does not exist: ${repoPath}`)
51
+
52
+ if (overrides && surface !== 'properties') {
53
+ log(`Note: --overrides applies to the properties surface only; ignoring it for ${surface}.`)
54
+ }
55
+
56
+ switch (surface) {
57
+ case 'properties': return previewProperty(repoPath, name, overrides, log)
58
+ case 'rpk': return previewRpk(repoPath, name, log)
59
+ case 'metrics':
60
+ case 'helm':
61
+ case 'crd':
62
+ case 'connect': return previewLintSurface(surface, repoPath, name, log)
63
+ default: throw new Error(`Unknown surface "${surface}". Supported: properties, rpk, metrics, helm, crd, connect`)
64
+ }
65
+ }
66
+
67
+ /* -------------------------------- properties ---------------------------- */
68
+
69
+ function previewProperty (repo, name, overridesPath, log) {
70
+ const properties = require('../lint-strings/surfaces/properties')
71
+ const handlebars = require('handlebars')
72
+ // Requiring the generator registers its Handlebars helpers (eq, ...) as a
73
+ // side effect - the same environment a real generation run uses.
74
+ require('../property-extractor/generate-handlebars-docs')
75
+
76
+ const json = properties.runExtractor(repo, log)
77
+ const prop = (json.properties || {})[name]
78
+ if (!prop) {
79
+ const near = Object.keys(json.properties || {}).filter((k) => k.includes(name)).slice(0, 8)
80
+ throw new Error(`Property "${name}" not found in ${repo}.${near.length > 0 ? ` Close matches: ${near.join(', ')}` : ''}`)
81
+ }
82
+
83
+ const templatePath = path.join(properties.TOOL_ROOT, 'templates', 'property.hbs')
84
+ const template = handlebars.compile(fs.readFileSync(templatePath, 'utf8'))
85
+ const asSource = template({ ...prop, name }).trim()
86
+
87
+ const spanNote = prop.line_start != null
88
+ ? `${prop.defined_in || ''}:${prop.line_start}-${prop.line_end != null ? prop.line_end : prop.line_start}`
89
+ : (prop.defined_in || '(unknown source location)')
90
+ let out = `Property: ${name} (${spanNote})\n\n`
91
+ out += pane('AS SOURCE (rendered from your checkout)', asSource)
92
+
93
+ if (overridesPath) {
94
+ const raw = JSON.parse(fs.readFileSync(path.resolve(overridesPath), 'utf8'))
95
+ // Accept both { properties: { name: {...} } } (docs-repo
96
+ // property-overrides.json) and a flat { name: {...} } map.
97
+ const entry = (raw.properties && raw.properties[name]) || (!raw.properties && raw[name]) || null
98
+ if (!entry) {
99
+ out += `\nNo override entry for "${name}" in ${overridesPath}: the source string ships as-is.\n`
100
+ } else {
101
+ const shipped = { ...prop, ...entry, name }
102
+ const asShipped = template(shipped).trim()
103
+ out += '\n' + pane('AS SHIPPED (override applied)', asShipped)
104
+ if (asShipped !== asSource) {
105
+ const maskedFields = Object.keys(entry).filter((k) => JSON.stringify(prop[k]) !== JSON.stringify(entry[k]))
106
+ out += `\nMASKED-BY-OVERRIDE: the docs repo overrides ${maskedFields.map((f) => `"${f}"`).join(', ') || 'this entry'} for ${name}.\n` +
107
+ 'Changes to the source string will NOT reach docs.redpanda.com until the override field is retired (fix the source string upstream, then delete the override field).\n'
108
+ } else {
109
+ out += '\nOverride entry present but the rendered output is identical: the override is redundant and can be retired.\n'
110
+ }
111
+ }
112
+ }
113
+ return out
114
+ }
115
+
116
+ /* ----------------------------------- rpk -------------------------------- */
117
+
118
+ /**
119
+ * Mirror the real generate-rpk-docs.js pipeline for one string:
120
+ * - Long: parseDescriptionSections first (ALLCAPS lines become "===" section
121
+ * headings, exactly as on the generated command page), then
122
+ * formatDescription + ensurePeriod on the main description and
123
+ * formatDescription on each section body.
124
+ * - Short: ensurePeriod(capToTwoSentences(formatDescription(...))) - the
125
+ * shortDesc path.
126
+ * - Flag usage: ensurePeriod(formatDescription(...)) - the flag table path.
127
+ */
128
+ function publishRpkString (kind, text, helpers) {
129
+ const { formatDescription, parseDescriptionSections, ensurePeriod, capToTwoSentences } = helpers
130
+ if (kind === 'long') {
131
+ const { mainDescription, sections } = parseDescriptionSections(text)
132
+ let out = ensurePeriod(formatDescription(mainDescription))
133
+ for (const [section, body] of Object.entries(sections)) {
134
+ const title = section.charAt(0).toUpperCase() + section.slice(1).toLowerCase()
135
+ out += `\n\n=== ${title}\n\n${formatDescription(body)}`
136
+ }
137
+ return out.trim()
138
+ }
139
+ if (kind === 'short') {
140
+ return ensurePeriod(capToTwoSentences(formatDescription(text, null, { skipTableConversion: true, skipListConversion: true }))).trim()
141
+ }
142
+ return ensurePeriod(formatDescription(text)).trim()
143
+ }
144
+
145
+ function previewRpk (repo, name, log) {
146
+ const rpk = require('../lint-strings/surfaces/rpk')
147
+ const helpers = require('../rpk-docs/generate-rpk-docs')
148
+
149
+ log(`Scanning rpk sources in ${repo}...`)
150
+ const decls = rpk.extract({ repo, log })
151
+ const isFlag = name.startsWith('-')
152
+ const target = isFlag ? name.replace(/^-+/, '') : name
153
+ const matches = decls.filter((d) => isFlag
154
+ ? (d.meta.kind === 'flag' && d.name === target)
155
+ : (d.meta.kind !== 'flag' && d.name === target))
156
+ if (matches.length === 0) {
157
+ throw new Error(`No rpk ${isFlag ? 'flag' : 'command'} named "${target}" found under src/go/rpk/pkg/cli. Use the leaf command token (for example "health") or --flag-name.`)
158
+ }
159
+
160
+ let out = ''
161
+ for (const d of matches.slice(0, 10)) {
162
+ const label = d.meta.kind === 'flag' ? `--${d.name} (flag usage)` : `${d.name} (${d.meta.kind})`
163
+ out += `rpk ${label} ${d.file}:${d.line_start}-${d.line_end}\n\n`
164
+ if (d.string == null) {
165
+ out += 'Source string is built dynamically (not a plain literal); preview unavailable.\n\n'
166
+ continue
167
+ }
168
+ out += pane('AS SOURCE', d.string)
169
+ out += '\n' + pane('AS PUBLISHED (after formatDescription)', publishRpkString(d.meta.kind, d.string, helpers)) + '\n'
170
+ }
171
+ if (matches.length > 10) out += `(${matches.length - 10} more matches not shown)\n`
172
+ return out
173
+ }
174
+
175
+ /* -------------------- metrics / helm / crd / connect --------------------- */
176
+
177
+ const SHAPES = {
178
+ // How each surface's description lands on the published page.
179
+ metrics: (d) => `=== ${d.name}\n\n${d.string}`,
180
+ helm: (d) => `=== ${d.name}\n\n${d.string}${d.meta.default_annotation ? `\n\n*Default:* ${d.meta.default_annotation}` : ''}`,
181
+ crd: (d) => `| *\`${d.name}\`* +\n(table cell in the CRD reference and \`kubectl explain\` output)\n| ${d.string}`,
182
+ connect: (d) => d.string // connect descriptions ARE the AsciiDoc page body
183
+ }
184
+
185
+ function previewLintSurface (surfaceName, repo, name, log) {
186
+ const { SURFACES } = require('../lint-strings')
187
+ const surface = SURFACES[surfaceName]
188
+ log(`Scanning ${surfaceName} sources in ${repo}...`)
189
+ const decls = surface.extract({ repo, log })
190
+ const matches = decls.filter((d) => d.name === name ||
191
+ (surfaceName === 'crd' && `${d.meta.struct}.${d.name}` === name))
192
+ if (matches.length === 0) {
193
+ throw new Error(`No ${surfaceName} declaration named "${name}" found in ${repo}.`)
194
+ }
195
+
196
+ let out = ''
197
+ for (const d of matches.slice(0, 10)) {
198
+ out += `${surfaceName}: ${d.name}${d.meta.struct ? ` (in ${d.meta.struct})` : ''} ${d.file}:${d.line_start}-${d.line_end}\n\n`
199
+ if (d.string == null) {
200
+ out += 'No description in source; this ships blank.\n\n'
201
+ continue
202
+ }
203
+ out += pane('AS PUBLISHED', SHAPES[surfaceName](d)) + '\n'
204
+ }
205
+ if (matches.length > 10) out += `(${matches.length - 10} more matches not shown)\n`
206
+ return out
207
+ }
208
+
209
+ /* ----------------------------------- CLI --------------------------------- */
210
+
211
+ function runCli (options) {
212
+ try {
213
+ process.stdout.write(previewString({
214
+ repo: options.repo,
215
+ surface: options.surface,
216
+ name: options.name,
217
+ overrides: options.overrides || null
218
+ }))
219
+ } catch (err) {
220
+ console.error(`Error: ${err.message}`)
221
+ process.exit(2)
222
+ }
223
+ process.exit(0)
224
+ }
225
+
226
+ module.exports = { previewString, publishRpkString, runCli }
227
+
228
+ // Direct usage: node tools/preview-string --repo <path> --surface <s> --name <n> [--overrides <path>]
229
+ if (require.main === module) {
230
+ const args = process.argv.slice(2)
231
+ const options = {}
232
+ for (let i = 0; i < args.length; i++) {
233
+ const arg = args[i]
234
+ if (arg === '--repo') options.repo = args[++i]
235
+ else if (arg === '--surface') options.surface = args[++i]
236
+ else if (arg === '--name') options.name = args[++i]
237
+ else if (arg === '--overrides') options.overrides = args[++i]
238
+ else {
239
+ console.error(`Unknown argument: ${arg}`)
240
+ console.error('Usage: node tools/preview-string --repo <path> --surface <s> --name <name> [--overrides <path>]')
241
+ process.exit(2)
242
+ }
243
+ }
244
+ runCli(options)
245
+ }
@@ -20,6 +20,11 @@ TOOL_ROOT := $(MODULE_ROOT)/tools/property-extractor
20
20
  TMP_ROOT := $(TOOL_ROOT)/tmp
21
21
  REDPANDA_SRC := $(TMP_ROOT)/redpanda
22
22
  TREESITTER_DIR:= $(TOOL_ROOT)/tree-sitter/tree-sitter-cpp
23
+ # One pin, read from the module that also bootstraps this grammar at run time
24
+ # (tools/lint-strings/surfaces/properties.js ensureTreesitter). Holding the tag
25
+ # in both places meant a bump here would silently leave the linter checking out
26
+ # a different grammar than the generator.
27
+ TREESITTER_TAG:= $(shell node -p "require('$(MODULE_ROOT)/tools/lint-strings/surfaces/properties.js').TREESITTER_TAG")
23
28
  VENV := $(TOOL_ROOT)/tmp/redpanda-property-extractor-venv
24
29
  PYTHON := $(VENV)/bin/python
25
30
  TREE_SITTER := npx tree-sitter
@@ -131,13 +136,13 @@ treesitter:
131
136
  git clone https://github.com/tree-sitter/tree-sitter-cpp.git "$(TREESITTER_DIR)"; \
132
137
  fi
133
138
  @CURRENT_TAG=$$(git -C "$(TREESITTER_DIR)" describe --tags --exact-match 2>/dev/null || true); \
134
- if [ "$$CURRENT_TAG" = "v0.20.5" ] && [ -f "$(TREESITTER_DIR)/src/parser.c" ]; then \
135
- echo "✅ tree-sitter-cpp parser already generated at v0.20.5; skipping regeneration"; \
139
+ if [ "$$CURRENT_TAG" = "$(TREESITTER_TAG)" ] && [ -f "$(TREESITTER_DIR)/src/parser.c" ]; then \
140
+ echo "✅ tree-sitter-cpp parser already generated at $(TREESITTER_TAG); skipping regeneration"; \
136
141
  else \
137
- echo "🔄 Ensuring tree-sitter-cpp is at compatible version v0.20.5…"; \
142
+ echo "🔄 Ensuring tree-sitter-cpp is at compatible version $(TREESITTER_TAG)…"; \
138
143
  cd "$(TREESITTER_DIR)" && \
139
144
  git fetch --tags -q && \
140
- git checkout -q v0.20.5 && \
145
+ git checkout -q $(TREESITTER_TAG) && \
141
146
  echo "🔧 Generating parser in $(TREESITTER_DIR)…" && \
142
147
  export CFLAGS="-Wno-unused-but-set-variable" && \
143
148
  npm install --silent && \
@@ -142,8 +142,12 @@ Create a `property-overrides.json` file to customize extracted documentation:
142
142
  "description": "Network address and port for Kafka API clients to connect to Redpanda brokers.",
143
143
  "example": ".Example\n[,yaml]\n----\nredpanda:\n kafka_api:\n - name: internal\n address: 0.0.0.0\n port: 9092\n - name: external\n address: redpanda.example.com\n port: 19092\n----",
144
144
  "version": "v21.4.1",
145
- "related_topics": [
146
- "xref:manage:kubernetes/networking/k-networking-and-connectivity.adoc[]"
145
+ "see_also": [
146
+ "xref:manage:kubernetes/networking/k-networking-and-connectivity.adoc[]",
147
+ {
148
+ "content": "xref:manage:cluster-maintenance/config-cluster.adoc[]",
149
+ "cloud_only": true
150
+ }
147
151
  ]
148
152
  }
149
153
  }
@@ -156,7 +160,17 @@ Create a `property-overrides.json` file to customize extracted documentation:
156
160
  * `example` - Add usage example in AsciiDoc format
157
161
  * `example_file` - Load example from external file
158
162
  * `version` - Document when property was introduced
159
- * `related_topics` - Array of AsciiDoc xrefs to related content
163
+ * `see_also` - Array of related links. Each item is either a plain string (shown in every build) or an object `{content, cloud_only}` / `{content, self_hosted_only}` (shown only in that build, wrapped with `ifdef::env-cloud[]`/`ifndef::env-cloud[]` at generation time). Setting both `cloud_only` and `self_hosted_only` on the same item is rejected by the schema — it would wrap the item in both directives at once, so it renders in neither build.
164
+ * `related_topics` - Deprecated predecessor to `see_also`. An array of plain strings, optionally prefixed `cloud-only:` / `self-managed-only:` for the same conditional behavior — that prefix is free text with no validation, which is why `see_also` replaced it. Existing `related_topics` entries keep working; new entries should use `see_also`.
165
+
166
+ Validate an overrides file against its schema (`docs-data/property-overrides.schema.json`) before generating docs:
167
+
168
+ [,bash]
169
+ ----
170
+ npx doc-tools validate property-overrides --overrides docs-data/property-overrides.json
171
+ ----
172
+
173
+ This catches malformed `see_also` items and unrecognized override keys — including a typo that would otherwise be silently dropped, since applying an override to an *existing* property has no catch-all pass-through (only overrides that create a new property from scratch do).
160
174
 
161
175
  === Understanding property changes
162
176
 
@@ -16,8 +16,7 @@ module.exports = {
16
16
  anchorName: require('./anchorName.js'),
17
17
  normalizePropertyAnchors: require('./normalizePropertyAnchors.js'),
18
18
  canonicalizePropertyXrefs: require('./canonicalizePropertyXrefs.js'),
19
- parseRelatedTopic: require('./parseRelatedTopic.js'),
20
- allTopicsConditional: require('./allTopicsConditional.js'),
19
+ seeAlsoView: require('./seeAlsoView.js'),
21
20
  capitalize: require('./capitalize.js'),
22
21
  length: require('./length.js'),
23
22
  };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Normalizes a property's related-topics data into one shape for the
5
+ * templates to render, regardless of which field or convention the override
6
+ * author used:
7
+ *
8
+ * - see_also (current): array of items, each either a plain string (shown
9
+ * everywhere) or {content, cloud_only, self_hosted_only} (shown only in
10
+ * the named build). Schema-validated — see docs-data/property-overrides.schema.json.
11
+ * - related_topics (deprecated, still read for back-compat with existing
12
+ * override files): array of strings, optionally prefixed with
13
+ * 'cloud-only:' / 'self-managed-only:' to get the same conditional
14
+ * behaviour. That prefix was free text with no validation; see_also
15
+ * replaces it with data a schema can check.
16
+ *
17
+ * @param {object} property - A property record, as extracted or overridden.
18
+ * @returns {Array<{content: string, cloudOnly: boolean, selfHostedOnly: boolean}>}
19
+ */
20
+ function normalizeSeeAlso(property) {
21
+ if (!property) return [];
22
+ const rawItems = Array.isArray(property.see_also)
23
+ ? property.see_also
24
+ : Array.isArray(property.related_topics)
25
+ ? property.related_topics
26
+ : [];
27
+
28
+ return rawItems
29
+ .map((item) => {
30
+ if (typeof item === 'string') {
31
+ const trimmed = item.trim();
32
+ if (trimmed.startsWith('cloud-only:')) {
33
+ return { content: trimmed.slice('cloud-only:'.length).trim(), cloudOnly: true, selfHostedOnly: false };
34
+ }
35
+ if (trimmed.startsWith('self-managed-only:')) {
36
+ return { content: trimmed.slice('self-managed-only:'.length).trim(), cloudOnly: false, selfHostedOnly: true };
37
+ }
38
+ return { content: trimmed, cloudOnly: false, selfHostedOnly: false };
39
+ }
40
+ if (item && typeof item === 'object' && typeof item.content === 'string') {
41
+ return {
42
+ content: item.content.trim(),
43
+ cloudOnly: item.cloud_only === true,
44
+ selfHostedOnly: item.self_hosted_only === true,
45
+ };
46
+ }
47
+ return null;
48
+ })
49
+ .filter((item) => item && item.content);
50
+ }
51
+
52
+ /**
53
+ * Builds the single view object property.hbs/topic-property.hbs render the
54
+ * "Related topics" row from: the normalized item list, plus which env-cloud
55
+ * branch (if any) every item shares, so the row's own header can be wrapped
56
+ * once instead of duplicated per item.
57
+ *
58
+ * sectionType is 'cloud' only when every item is cloud-only, 'self-managed'
59
+ * only when every item is self-hosted-only, and 'normal' otherwise (a mix of
60
+ * conditional and/or unconditional items, each wrapped individually).
61
+ *
62
+ * @param {object} property - A property record, as extracted or overridden.
63
+ * @returns {{items: Array, sectionType: ('cloud'|'self-managed'|'normal')}}
64
+ */
65
+ function seeAlsoView(property) {
66
+ const items = normalizeSeeAlso(property);
67
+ let sectionType = 'normal';
68
+ if (items.length > 0) {
69
+ if (items.every((item) => item.cloudOnly)) sectionType = 'cloud';
70
+ else if (items.every((item) => item.selfHostedOnly)) sectionType = 'self-managed';
71
+ }
72
+ return { items, sectionType };
73
+ }
74
+
75
+ module.exports = seeAlsoView;
76
+ module.exports.normalizeSeeAlso = normalizeSeeAlso;
@@ -362,8 +362,29 @@ def parse_cpp_source(treesitter_parser, cpp_language, source_code):
362
362
  parameters = PropertyBag()
363
363
  current_field = None
364
364
  seen_first_argument = False
365
+ # Line spans (1-indexed, inclusive) of each full member-initializer entry
366
+ # (`name(arg, arg, ...)`), keyed by field name. Recorded so downstream
367
+ # consumers (for example, doc-string linting and PR suggestion blocks) can
368
+ # anchor a property to its exact source lines. Additive: existing
369
+ # consumers of the output JSON are unaffected.
370
+ declaration_spans = {}
365
371
 
366
372
  for node, label in captures:
373
+ # The SOURCE_QUERY also captures the whole field_initializer node as
374
+ # @field. Use it to record the declaration's line span; the entry is
375
+ # keyed by its field_identifier child so this works regardless of the
376
+ # order tree-sitter returns same-start captures in.
377
+ if label == "field" and node.type == "field_initializer":
378
+ name_node = next(
379
+ (c for c in node.children if c.type == "field_identifier"), None
380
+ )
381
+ if name_node is not None:
382
+ declaration_spans[name_node.text.decode("utf-8")] = (
383
+ node.start_point[0] + 1,
384
+ node.end_point[0] + 1,
385
+ )
386
+ continue
387
+
367
388
  # Start of a new field initializer
368
389
  if label == "field" and node.type == "field_identifier":
369
390
  current_field = node.text.decode("utf-8")
@@ -391,6 +412,11 @@ def parse_cpp_source(treesitter_parser, cpp_language, source_code):
391
412
  if normalized_param:
392
413
  parameters[current_field]["params"].append(normalized_param)
393
414
 
415
+ for field_name, (line_start, line_end) in declaration_spans.items():
416
+ if field_name in parameters:
417
+ parameters[field_name]["line_start"] = line_start
418
+ parameters[field_name]["line_end"] = line_end
419
+
394
420
  return parameters
395
421
 
396
422