@redpanda-data/docs-extensions-and-macros 5.13.5 → 5.17.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 +349 -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/sync-schemas.js +144 -0
- package/docs-data/rpk-overrides.schema.json +4 -0
- 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/parser.py +26 -0
- package/tools/property-extractor/property_extractor.py +41 -0
- package/tools/property-extractor/transformers.py +15 -2
- package/tools/rpk-docs/detect-platform-commands.js +8 -1
- package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
|
@@ -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 && \
|
|
@@ -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
|
|
|
@@ -963,6 +963,44 @@ def clean_private_fields_from_definitions(definitions):
|
|
|
963
963
|
|
|
964
964
|
|
|
965
965
|
# The definitions.json file contains type definitions that the extractor uses to standardize and centralize type information. After extracting and transforming the properties from the source code, the function merge_properties_and_definitions looks up each property's type in the definitions. If a property's type (or the type of its items, in the case of arrays) matches one of the definitions, the transformer replaces that type with a JSON pointer ( such as #/definitions/<type>) to the corresponding entry in definitions.json. The final JSON output then includes both a properties section (with types now referencing the definitions) and a definitions section, so that consumers of the output can easily resolve the full type information.
|
|
966
|
+
# Fields recorded for tooling only. lint-strings and preview-string read them
|
|
967
|
+
# from the raw --output to anchor a finding to its declaration; nothing on the
|
|
968
|
+
# docs site does. The enhanced JSON is copied verbatim into
|
|
969
|
+
# modules/reference/attachments and downloaded by every browser that hovers a
|
|
970
|
+
# prop: macro, so ~40 bytes per property (~56KB across the real ~1400-property
|
|
971
|
+
# file) would be paid by readers for data no reader uses.
|
|
972
|
+
TOOLING_ONLY_PROPERTY_FIELDS = ("line_start", "line_end")
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def strip_tooling_only_fields(properties):
|
|
976
|
+
"""
|
|
977
|
+
Remove tooling-only span fields from properties bound for the PUBLISHED
|
|
978
|
+
JSON attachment.
|
|
979
|
+
|
|
980
|
+
Only the enhanced output is published; the raw --output keeps the spans,
|
|
981
|
+
because that is the file lint-strings and preview-string consume.
|
|
982
|
+
|
|
983
|
+
Args:
|
|
984
|
+
properties: Dictionary of properties
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
The same dictionary, with the tooling-only fields removed
|
|
988
|
+
"""
|
|
989
|
+
removed = 0
|
|
990
|
+
for prop in properties.values():
|
|
991
|
+
if not isinstance(prop, dict):
|
|
992
|
+
continue
|
|
993
|
+
for field in TOOLING_ONLY_PROPERTY_FIELDS:
|
|
994
|
+
if field in prop:
|
|
995
|
+
del prop[field]
|
|
996
|
+
removed += 1
|
|
997
|
+
if removed:
|
|
998
|
+
logging.info(
|
|
999
|
+
f"Stripped {removed} tooling-only span field(s) from the published properties JSON"
|
|
1000
|
+
)
|
|
1001
|
+
return properties
|
|
1002
|
+
|
|
1003
|
+
|
|
966
1004
|
def merge_properties_and_definitions(properties, definitions):
|
|
967
1005
|
# Do not overwrite the resolved type/default with a reference. Just return the resolved properties and definitions.
|
|
968
1006
|
return dict(properties=properties, definitions=definitions)
|
|
@@ -3143,6 +3181,9 @@ def main():
|
|
|
3143
3181
|
# 7. Clean private fields from definitions (keep JSON output clean)
|
|
3144
3182
|
filtered_enhanced_definitions = clean_private_fields_from_definitions(filtered_enhanced_definitions)
|
|
3145
3183
|
|
|
3184
|
+
# 8. Drop tooling-only spans: this JSON is the published attachment.
|
|
3185
|
+
enhanced_properties = strip_tooling_only_fields(enhanced_properties)
|
|
3186
|
+
|
|
3146
3187
|
# Generate enhanced properties JSON (with overrides)
|
|
3147
3188
|
enhanced_properties_and_definitions = merge_properties_and_definitions(
|
|
3148
3189
|
enhanced_properties, filtered_enhanced_definitions
|
|
@@ -655,8 +655,10 @@ class BasicInfoTransformer:
|
|
|
655
655
|
EXPECTED FINAL RESULT:
|
|
656
656
|
{
|
|
657
657
|
"name": "property_name", # Clean identifier for JSON keys
|
|
658
|
-
"description": "Human readable...", # Description for documentation
|
|
659
|
-
"defined_in": "src/v/config/..."
|
|
658
|
+
"description": "Human readable...", # Description for documentation
|
|
659
|
+
"defined_in": "src/v/config/...", # Normalized file path
|
|
660
|
+
"line_start": 1852, # First line of the declaration (1-indexed)
|
|
661
|
+
"line_end": 1858 # Last line of the declaration (inclusive)
|
|
660
662
|
}
|
|
661
663
|
|
|
662
664
|
DOWNSTREAM USAGE:
|
|
@@ -724,6 +726,17 @@ class BasicInfoTransformer:
|
|
|
724
726
|
property["defined_in"] = re.sub(
|
|
725
727
|
r"^.*src/", "src/", str(file_pair.implementation)
|
|
726
728
|
)
|
|
729
|
+
|
|
730
|
+
# --- Step 4: line span (additive) ---
|
|
731
|
+
# 1-indexed, inclusive lines of the full member-initializer entry in
|
|
732
|
+
# the implementation file named by defined_in. Recorded by the parser
|
|
733
|
+
# (parse_cpp_source) from the tree-sitter field_initializer node.
|
|
734
|
+
# Spans the whole declaration, not just the description literal, so
|
|
735
|
+
# diff-anchored tooling can match clang-format-wrapped strings.
|
|
736
|
+
if info.get("line_start") is not None:
|
|
737
|
+
property["line_start"] = info["line_start"]
|
|
738
|
+
property["line_end"] = info.get("line_end", info["line_start"])
|
|
739
|
+
|
|
727
740
|
return property
|
|
728
741
|
|
|
729
742
|
@debug_transformer
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
const fs = require('fs')
|
|
49
49
|
const path = require('path')
|
|
50
|
+
const { maskComments } = require('../lint-strings/go-source')
|
|
50
51
|
|
|
51
52
|
/** GOOS values recognized in filename suffixes and build tags */
|
|
52
53
|
const KNOWN_GOOS = new Set([
|
|
@@ -248,7 +249,13 @@ function commandNameFromConstructor(funcName) {
|
|
|
248
249
|
* @param {string} content - Go file content
|
|
249
250
|
* @returns {Object<string, {useName: string|null, excluded: boolean}>}
|
|
250
251
|
*/
|
|
251
|
-
function parseConstructors(
|
|
252
|
+
function parseConstructors(rawContent) {
|
|
253
|
+
// Mask comments first. Scanning raw text read a `Use: "..."` out of a
|
|
254
|
+
// commented-out or example command and reported it as the real one; the
|
|
255
|
+
// masker blanks comment bodies while preserving offsets, line breaks and
|
|
256
|
+
// every string literal, so the regexes below see only live code. Shared with
|
|
257
|
+
// the lint-strings Go surfaces rather than reimplemented here.
|
|
258
|
+
const content = maskComments(rawContent)
|
|
252
259
|
const constructors = {}
|
|
253
260
|
const funcRe = /^func\s+(\w+)\s*\([^)]*\)\s*\*cobra\.Command\s*\{/gm
|
|
254
261
|
const matches = []
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { maskComments, findBalancedClose } = require('../lint-strings/go-source');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Scan a Go file for cobra command definitions with deprecation/hidden status
|
|
@@ -17,31 +18,27 @@ function scanGoFile(filePath) {
|
|
|
17
18
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
18
19
|
|
|
19
20
|
// Look for NewCommand function that returns *cobra.Command
|
|
20
|
-
|
|
21
|
+
// Mask comments before any scanning. The hand-rolled brace counter this
|
|
22
|
+
// replaced was comment- and string-blind, so a `{` inside a comment
|
|
23
|
+
// unbalanced it and truncated the cobra.Command block early, and the field
|
|
24
|
+
// regexes then read a `Use:` string out of a commented-out example. Both
|
|
25
|
+
// helpers are string-aware and already shared by the lint-strings Go
|
|
26
|
+
// surfaces; the masker preserves offsets and every string literal.
|
|
27
|
+
const masked = maskComments(content);
|
|
28
|
+
|
|
29
|
+
const newCommandMatch = masked.match(/func NewCommand\([^)]*\)\s*\*cobra\.Command\s*\{/);
|
|
21
30
|
if (!newCommandMatch) {
|
|
22
31
|
return null;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
// Extract the cobra.Command struct definition
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let braceCount = 0;
|
|
29
|
-
let startIndex = content.indexOf('&cobra.Command{');
|
|
30
|
-
|
|
31
|
-
if (startIndex === -1) {
|
|
35
|
+
const structIndex = masked.indexOf('&cobra.Command{');
|
|
36
|
+
if (structIndex === -1) {
|
|
32
37
|
return null;
|
|
33
38
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const char = content[i];
|
|
38
|
-
if (char === '{') braceCount++;
|
|
39
|
-
if (char === '}') {
|
|
40
|
-
if (braceCount === 0) break;
|
|
41
|
-
braceCount--;
|
|
42
|
-
}
|
|
43
|
-
cmdBlock += char;
|
|
44
|
-
}
|
|
39
|
+
const openIndex = structIndex + '&cobra.Command'.length;
|
|
40
|
+
const closeIndex = findBalancedClose(masked, openIndex, '{', '}');
|
|
41
|
+
const cmdBlock = masked.slice(openIndex + 1, closeIndex === -1 ? masked.length : closeIndex);
|
|
45
42
|
|
|
46
43
|
// Extract fields - handle multiline strings with backticks or quotes
|
|
47
44
|
const useMatch = cmdBlock.match(/Use:\s*"([^"]+)"/);
|