@redpanda-data/docs-extensions-and-macros 5.14.0 → 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.
Files changed (44) hide show
  1. package/bin/doc-tools-mcp.js +88 -0
  2. package/bin/doc-tools.js +252 -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/package.json +2 -1
  10. package/tools/generate-cli-docs.js +28 -67
  11. package/tools/lint-strings/diff.js +137 -0
  12. package/tools/lint-strings/engine.js +163 -0
  13. package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
  14. package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
  15. package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
  16. package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
  17. package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
  18. package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
  19. package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
  20. package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
  21. package/tools/lint-strings/go-source.js +327 -0
  22. package/tools/lint-strings/index.js +229 -0
  23. package/tools/lint-strings/rules/common.js +148 -0
  24. package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
  25. package/tools/lint-strings/source-text.js +41 -0
  26. package/tools/lint-strings/surfaces/connect.js +233 -0
  27. package/tools/lint-strings/surfaces/crd.js +239 -0
  28. package/tools/lint-strings/surfaces/helm.js +159 -0
  29. package/tools/lint-strings/surfaces/metrics.js +368 -0
  30. package/tools/lint-strings/surfaces/properties.js +216 -0
  31. package/tools/lint-strings/surfaces/rpk.js +377 -0
  32. package/tools/overrides-audit/README.adoc +152 -0
  33. package/tools/overrides-audit/adapters/connect.js +144 -0
  34. package/tools/overrides-audit/adapters/properties.js +117 -0
  35. package/tools/overrides-audit/adapters/rpk.js +192 -0
  36. package/tools/overrides-audit/classify.js +546 -0
  37. package/tools/overrides-audit/index.js +185 -0
  38. package/tools/preview-string/index.js +245 -0
  39. package/tools/property-extractor/Makefile +9 -4
  40. package/tools/property-extractor/parser.py +26 -0
  41. package/tools/property-extractor/property_extractor.py +41 -0
  42. package/tools/property-extractor/transformers.py +15 -2
  43. package/tools/rpk-docs/detect-platform-commands.js +8 -1
  44. package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
@@ -0,0 +1,239 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const yaml = require('js-yaml')
6
+
7
+ const { SourceCache } = require('../source-text')
8
+ const { collectGoFiles } = require('../go-source')
9
+
10
+ /**
11
+ * CRD surface: Go doc comments above struct fields in the operator API
12
+ * types (operator/api/redpanda/...). These comments ship verbatim to the
13
+ * generated CRD reference on docs.redpanda.com AND to `kubectl explain`.
14
+ *
15
+ * User-facing filtering mirrors crd-ref-docs: the surface reads
16
+ * operator/crd-ref-docs-config.yaml for the `hidefromdoc` custom marker and
17
+ * the processor ignoreTypes/ignoreFields regexes, so fields the generator
18
+ * never documents are never linted.
19
+ *
20
+ * Marker lines (+kubebuilder:..., +optional, +required, +genclient, and any
21
+ * other +directive) are stripped before the prose is judged.
22
+ *
23
+ * The signature bad pattern here is a description that leads with the Go
24
+ * field identifier ("ClusterSource is a reference to...") when users type
25
+ * the json key ("cluster") - the docs and kubectl explain both show the
26
+ * json name, so the Go name means nothing to readers.
27
+ */
28
+
29
+ const CONVENTION = {
30
+ case: 'sentence',
31
+ terminal_period: true,
32
+ verbatim_asciidoc: true
33
+ }
34
+
35
+ const API_ROOT = path.join('operator', 'api', 'redpanda')
36
+ const CONFIG_PATH = path.join('operator', 'crd-ref-docs-config.yaml')
37
+
38
+ /**
39
+ * Compile a list of regex source strings, skipping (with a warning) any
40
+ * pattern that isn't valid JS regex syntax instead of throwing. The config
41
+ * file is written for a Go tool (crd-ref-docs), so it can legally contain
42
+ * Go/RE2-only constructs like `(?i)` or `(?P<name>...)` that JS's RegExp
43
+ * rejects - one such pattern must not abort the entire lint run.
44
+ */
45
+ function compilePatterns (patterns, configKey) {
46
+ const compiled = []
47
+ for (const p of patterns) {
48
+ try {
49
+ compiled.push(new RegExp(p))
50
+ } catch (err) {
51
+ // eslint-disable-next-line no-console
52
+ console.warn(`lint-strings: skipping invalid ${configKey} pattern in ${CONFIG_PATH} (${JSON.stringify(p)}): ${err.message}`)
53
+ }
54
+ }
55
+ return compiled
56
+ }
57
+
58
+ /** Load ignore rules from crd-ref-docs-config.yaml (absent file = no rules). */
59
+ function loadConfig (repo) {
60
+ const configPath = path.join(repo, CONFIG_PATH)
61
+ const config = { ignoreTypes: [], ignoreFields: [], hiddenMarker: 'hidefromdoc' }
62
+ if (!fs.existsSync(configPath)) return config
63
+ const parsed = yaml.load(fs.readFileSync(configPath, 'utf8')) || {}
64
+ const processor = parsed.processor || {}
65
+ config.ignoreTypes = compilePatterns(processor.ignoreTypes || [], 'processor.ignoreTypes')
66
+ config.ignoreFields = compilePatterns(processor.ignoreFields || [], 'processor.ignoreFields')
67
+ return config
68
+ }
69
+
70
+ function matchesAny (patterns, ...candidates) {
71
+ return patterns.some((pattern) => candidates.some((c) => pattern.test(c)))
72
+ }
73
+
74
+ /**
75
+ * Parse one Go file's struct fields. Exported for tests.
76
+ *
77
+ * @param {string} content - File content
78
+ * @param {string} file - Repo-relative path
79
+ * @param {Object} config - From loadConfig
80
+ * @returns {Array} declarations (without declaration_text)
81
+ */
82
+ function scanFile (content, file, config = { ignoreTypes: [], ignoreFields: [], hiddenMarker: 'hidefromdoc' }) {
83
+ const declarations = []
84
+ const lines = content.split('\n')
85
+ const packageMatch = content.match(/^package\s+(\w+)/m)
86
+ const pkg = packageMatch ? packageMatch[1] : ''
87
+
88
+ let comment = [] // pending comment lines: { line (0-indexed), text }
89
+ let struct = null // { name, hidden } while inside a struct body
90
+ let depth = 0
91
+
92
+ for (let i = 0; i < lines.length; i++) {
93
+ const line = lines[i]
94
+ const trimmed = line.trim()
95
+
96
+ if (struct === null) {
97
+ if (/^\s*\/\//.test(line)) {
98
+ comment.push({ line: i, text: trimmed.replace(/^\/\/\s?/, '') })
99
+ continue
100
+ }
101
+ const typeMatch = line.match(/^type\s+([A-Za-z0-9_]+)\s+struct\s*\{/)
102
+ if (typeMatch) {
103
+ const name = typeMatch[1]
104
+ const hidden = comment.some((c) => c.text.trim().startsWith(`+${config.hiddenMarker}`)) ||
105
+ matchesAny(config.ignoreTypes, name, `${pkg}.${name}`)
106
+ struct = { name, hidden, exported: /^[A-Z]/.test(name) }
107
+ depth = 1
108
+ comment = []
109
+ continue
110
+ }
111
+ if (trimmed !== '') comment = []
112
+ continue
113
+ }
114
+
115
+ // Inside a struct body.
116
+ if (/^\s*\/\//.test(line)) {
117
+ comment.push({ line: i, text: trimmed.replace(/^\/\/\s?/, '') })
118
+ continue
119
+ }
120
+ if (trimmed === '') {
121
+ comment = []
122
+ continue
123
+ }
124
+
125
+ // Track nested braces (anonymous struct fields) without leaving the type.
126
+ const opens = (trimmed.match(/\{/g) || []).length
127
+ const closes = (trimmed.match(/\}/g) || []).length
128
+ const fieldMatch = line.match(/^\s*([A-Z][A-Za-z0-9_]*)\s+\S/)
129
+ if (fieldMatch && depth === 1 && !struct.hidden && struct.exported) {
130
+ const goName = fieldMatch[1]
131
+ const tagMatch = line.match(/`[^`]*json:"([^",]*)[^`]*`/)
132
+ const jsonName = tagMatch ? tagMatch[1] : null
133
+ const hiddenField = comment.some((c) => c.text.trim().startsWith(`+${config.hiddenMarker}`))
134
+ // Only json-serialized, non-inline, non-ignored fields ship to users.
135
+ if (jsonName && jsonName !== '-' && !hiddenField &&
136
+ !matchesAny(config.ignoreFields, goName, goName.toLowerCase(), jsonName)) {
137
+ const prose = comment
138
+ .filter((c) => !c.text.trim().startsWith('+'))
139
+ .map((c) => c.text)
140
+ .join('\n')
141
+ .trim()
142
+ declarations.push({
143
+ surface: 'crd',
144
+ name: jsonName,
145
+ file,
146
+ line_start: (comment.length > 0 ? comment[0].line : i) + 1,
147
+ line_end: i + 1,
148
+ string: prose || null,
149
+ declaration_text: null,
150
+ convention: CONVENTION,
151
+ meta: {
152
+ kind: 'field',
153
+ struct: struct.name,
154
+ go_name: goName,
155
+ json_name: jsonName
156
+ }
157
+ })
158
+ }
159
+ }
160
+ comment = []
161
+ depth += opens - closes
162
+ if (depth <= 0) struct = null
163
+ }
164
+
165
+ return declarations
166
+ }
167
+
168
+ /**
169
+ * Extract CRD field declarations.
170
+ *
171
+ * @param {Object} options - { repo, files (Set of repo-relative paths, diff
172
+ * mode; when omitted, scans operator/api/redpanda), log }
173
+ */
174
+ function extract ({ repo, files = null }) {
175
+ const config = loadConfig(repo)
176
+ let fileList
177
+ if (files) {
178
+ fileList = [...files].filter((f) => f.endsWith('.go') && !f.endsWith('_test.go') && !path.basename(f).startsWith('zz_generated'))
179
+ } else {
180
+ fileList = collectGoFiles(path.join(repo, API_ROOT))
181
+ .filter((f) => !path.basename(f).startsWith('zz_generated'))
182
+ .map((f) => path.join(API_ROOT, f))
183
+ }
184
+
185
+ const cache = new SourceCache(repo)
186
+ const declarations = []
187
+ for (const file of fileList) {
188
+ const absPath = path.isAbsolute(file) ? file : path.join(repo, file)
189
+ if (!fs.existsSync(absPath)) continue
190
+ const content = fs.readFileSync(absPath, 'utf8')
191
+ if (!content.includes('struct')) continue
192
+ for (const decl of scanFile(content, file, config)) {
193
+ decl.declaration_text = cache.span(file, decl.line_start, decl.line_end)
194
+ declarations.push(decl)
195
+ }
196
+ }
197
+ return declarations
198
+ }
199
+
200
+ /** Surface-specific convention rules. */
201
+ const RULES = [
202
+ {
203
+ name: 'undocumented-field',
204
+ description: 'Exported user-facing field with no doc comment',
205
+ severity: 'warning',
206
+ check: (decl) => {
207
+ if (decl.string !== null) return []
208
+ return [{ message: `Field "${decl.meta.go_name}" (json: "${decl.name}") in ${decl.meta.struct} has no doc comment. It ships blank in the CRD reference and in kubectl explain.` }]
209
+ }
210
+ },
211
+ {
212
+ name: 'go-field-name-first',
213
+ description: 'Description leads with the Go field name instead of the YAML key',
214
+ severity: 'warning',
215
+ check: (decl) => {
216
+ const text = (decl.string || '').trim()
217
+ if (!text) return []
218
+ const firstWord = (text.match(/^[A-Za-z0-9_]+/) || [null])[0]
219
+ if (!firstWord) return []
220
+ if (firstWord === decl.meta.go_name &&
221
+ decl.meta.go_name.toLowerCase() !== decl.meta.json_name.toLowerCase()) {
222
+ return [{ message: `Description starts with the Go field name "${decl.meta.go_name}", but users type "${decl.meta.json_name}" in YAML. Describe the json key: "${decl.meta.json_name}" (or start with what the field does).` }]
223
+ }
224
+ return []
225
+ }
226
+ }
227
+ ]
228
+
229
+ module.exports = {
230
+ name: 'crd',
231
+ convention: CONVENTION,
232
+ extract,
233
+ scanFile,
234
+ loadConfig,
235
+ rules: RULES,
236
+ // Missing prose is surfaced by the crd-specific undocumented-field rule
237
+ // (warning, per the docs contract) instead of the generic error.
238
+ skipRules: ['empty-description']
239
+ }
@@ -0,0 +1,159 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const { SourceCache } = require('../source-text')
7
+ const { parseValuesFile: parseSharedValuesFile } = require('../../../cli-utils/helm-commented-values')
8
+
9
+ /**
10
+ * Helm surface: helm-docs description comments in chart values.yaml files
11
+ * (charts/<name>/chart/values.yaml, plus charts/<name>/values.yaml for
12
+ * charts without the chart/ subdir).
13
+ *
14
+ * Documentation conventions - the union of what actually ships:
15
+ * - helm-docs: `# -- description` (with plain `# ...` continuation lines)
16
+ * attaches to the real key DIRECTLY below the comment block; `# @default
17
+ * -- text`, `# @raw`, and `# @ignored` annotate it.
18
+ * - doc-tools helm-spec (cli-utils/helm-commented-values.js): a `# --`
19
+ * block directly above a COMMENTED-OUT key documents that key, and
20
+ * `# @doc full.path -- description` documents any path explicitly.
21
+ *
22
+ * Both conventions are decided by ONE walk, cli-utils/helm-commented-values
23
+ * `parseValuesFile`, which this surface calls with attachRealKeys so it also
24
+ * models helm-docs' own real-key attachment. This file used to hold a
25
+ * byte-identical copy of that module's nine regexes and a near-copy of its
26
+ * state machine, on the stated grounds that "the linter never calls a marker
27
+ * dead that the generator in this repo renders" - which nothing enforced.
28
+ * Sharing the walk is what actually enforces it.
29
+ *
30
+ * A DEAD marker is a `# --` block neither pipeline can attach: one buried
31
+ * inside another commented-out key's subtree (the classic commented-out
32
+ * example block), or one separated from any key by a blank line. Its
33
+ * description silently never ships - that is the error this surface exists
34
+ * to catch.
35
+ *
36
+ * helm-docs output is markdown converted via pandoc, not verbatim AsciiDoc,
37
+ * so the verbatim escaping rules do not apply. Terminal periods are
38
+ * optional prose style here; only capitalization is enforced (via the
39
+ * common starts-lowercase rule).
40
+ */
41
+
42
+ const CONVENTION = {
43
+ case: 'sentence',
44
+ terminal_period: 'optional',
45
+ verbatim_asciidoc: false
46
+ }
47
+
48
+ /**
49
+ * Parse one values.yaml into lint declarations. Exported for tests.
50
+ *
51
+ * The attachment decisions and the line spans both come from the shared walk;
52
+ * this function only reshapes its records into the linter's declaration form.
53
+ *
54
+ * @param {string} content - File content
55
+ * @param {string} file - Repo-relative path
56
+ * @returns {Array} declarations (without declaration_text)
57
+ */
58
+ function parseValuesFile (content, file) {
59
+ return parseSharedValuesFile(content, { attachRealKeys: true }).map((r) => {
60
+ // The generator keeps newlines in a description; helm-docs renders one
61
+ // paragraph, and the rules read one string, so flatten here.
62
+ const string = r.descLines.map((l) => l.trim()).filter(Boolean).join(' ') || null
63
+ let meta
64
+ if (r.kind === 'dead-marker') {
65
+ meta = { kind: 'dead-marker', unverifiable: true }
66
+ } else if (r.undocumented) {
67
+ // Nothing to lint for content: the rule fires on the absence itself.
68
+ meta = { kind: 'key', top_level: true, undocumented: true, unverifiable: true }
69
+ } else if (r.commentedOut) {
70
+ meta = { kind: 'key', commented_out: true, top_level: r.topLevel, default_annotation: r.default || null }
71
+ } else {
72
+ meta = { kind: 'key', top_level: r.topLevel, raw: Boolean(r.annotations.raw), default_annotation: r.default || null }
73
+ }
74
+ return {
75
+ surface: 'helm',
76
+ name: r.kind === 'dead-marker' ? null : r.path,
77
+ file,
78
+ line_start: r.lineStart + 1, // 0-indexed -> 1-indexed
79
+ line_end: r.lineEnd + 1,
80
+ string: r.undocumented ? null : string,
81
+ declaration_text: null,
82
+ convention: CONVENTION,
83
+ meta
84
+ }
85
+ })
86
+ }
87
+
88
+ /**
89
+ * Extract helm declarations.
90
+ *
91
+ * @param {Object} options - { repo, files (Set of repo-relative paths, diff
92
+ * mode; when omitted, scans charts/<*>/chart/values.yaml and
93
+ * charts/<*>/values.yaml), log }
94
+ */
95
+ function extract ({ repo, files = null }) {
96
+ let fileList
97
+ if (files) {
98
+ fileList = [...files]
99
+ } else {
100
+ fileList = []
101
+ const chartsRoot = path.join(repo, 'charts')
102
+ if (fs.existsSync(chartsRoot)) {
103
+ for (const entry of fs.readdirSync(chartsRoot, { withFileTypes: true })) {
104
+ if (!entry.isDirectory()) continue
105
+ for (const candidate of [
106
+ path.join('charts', entry.name, 'chart', 'values.yaml'),
107
+ path.join('charts', entry.name, 'values.yaml')
108
+ ]) {
109
+ if (fs.existsSync(path.join(repo, candidate))) fileList.push(candidate)
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ const cache = new SourceCache(repo)
116
+ const declarations = []
117
+ for (const file of fileList) {
118
+ const absPath = path.isAbsolute(file) ? file : path.join(repo, file)
119
+ if (!fs.existsSync(absPath)) continue
120
+ const content = fs.readFileSync(absPath, 'utf8')
121
+ for (const decl of parseValuesFile(content, file)) {
122
+ decl.declaration_text = cache.span(file, decl.line_start, decl.line_end)
123
+ declarations.push(decl)
124
+ }
125
+ }
126
+ return declarations
127
+ }
128
+
129
+ /** Surface-specific convention rules. */
130
+ const RULES = [
131
+ {
132
+ name: 'dead-marker',
133
+ description: 'A # -- marker no docs pipeline can attach to any key',
134
+ severity: 'error',
135
+ runOnUnverifiable: true,
136
+ check: (decl) => {
137
+ if (decl.meta.kind !== 'dead-marker') return []
138
+ return [{ message: 'This "# --" description is buried where neither helm-docs nor the helm-spec commented-values pass can attach it (inside another commented-out key\'s subtree, or separated from any key), so it silently never ships. Move it directly above the key it documents, use "# @doc full.path -- ...", or delete it.' }]
139
+ }
140
+ },
141
+ {
142
+ name: 'undocumented-top-level-key',
143
+ description: 'Top-level user-visible key with no # -- description',
144
+ severity: 'info',
145
+ runOnUnverifiable: true,
146
+ check: (decl) => {
147
+ if (decl.meta.kind !== 'key' || !decl.meta.undocumented) return []
148
+ return [{ message: `Top-level key "${decl.name}" has no "# --" description, so helm-docs ships it undocumented. Add a marker comment directly above it (or "# @ignored" if it is not user-facing).` }]
149
+ }
150
+ }
151
+ ]
152
+
153
+ module.exports = {
154
+ name: 'helm',
155
+ convention: CONVENTION,
156
+ extract,
157
+ parseValuesFile,
158
+ rules: RULES
159
+ }