@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.
- package/bin/doc-tools-mcp.js +88 -0
- package/bin/doc-tools.js +334 -102
- package/bin/mcp-tools/index.js +20 -0
- package/bin/mcp-tools/lint-doc-strings.js +75 -0
- package/bin/mcp-tools/overrides-audit.js +100 -0
- package/bin/mcp-tools/preview-doc-string.js +71 -0
- package/cli-utils/diff-utils.js +77 -0
- package/cli-utils/helm-commented-values.js +172 -23
- package/cli-utils/validation-result.js +53 -0
- package/macros/REFERENCE.adoc +3 -2
- package/macros/prop.js +34 -6
- package/package.json +2 -1
- package/tools/generate-cli-docs.js +28 -67
- package/tools/lint-strings/diff.js +137 -0
- package/tools/lint-strings/engine.js +163 -0
- package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
- package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
- package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
- package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
- package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
- package/tools/lint-strings/go-source.js +327 -0
- package/tools/lint-strings/index.js +229 -0
- package/tools/lint-strings/rules/common.js +148 -0
- package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
- package/tools/lint-strings/source-text.js +41 -0
- package/tools/lint-strings/surfaces/connect.js +233 -0
- package/tools/lint-strings/surfaces/crd.js +239 -0
- package/tools/lint-strings/surfaces/helm.js +159 -0
- package/tools/lint-strings/surfaces/metrics.js +368 -0
- package/tools/lint-strings/surfaces/properties.js +216 -0
- package/tools/lint-strings/surfaces/rpk.js +377 -0
- package/tools/overrides-audit/README.adoc +152 -0
- package/tools/overrides-audit/adapters/connect.js +144 -0
- package/tools/overrides-audit/adapters/properties.js +117 -0
- package/tools/overrides-audit/adapters/rpk.js +192 -0
- package/tools/overrides-audit/classify.js +546 -0
- package/tools/overrides-audit/index.js +185 -0
- package/tools/preview-string/index.js +245 -0
- package/tools/property-extractor/Makefile +9 -4
- package/tools/property-extractor/README.adoc +17 -3
- package/tools/property-extractor/helpers/index.js +1 -2
- package/tools/property-extractor/helpers/seeAlsoView.js +76 -0
- package/tools/property-extractor/parser.py +26 -0
- package/tools/property-extractor/property_extractor.py +65 -6
- package/tools/property-extractor/templates/property.hbs +25 -37
- package/tools/property-extractor/templates/topic-property.hbs +25 -37
- package/tools/property-extractor/transformers.py +15 -2
- package/tools/property-extractor/validate-overrides.js +120 -0
- package/tools/rpk-docs/detect-platform-commands.js +8 -1
- package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
- package/tools/rpk-docs/validate-overrides.js +1 -45
- package/tools/property-extractor/helpers/allTopicsConditional.js +0 -19
- 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" = "
|
|
135
|
-
echo "✅ tree-sitter-cpp parser already generated at
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
* `
|
|
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
|
-
|
|
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
|
|