@redpanda-data/docs-extensions-and-macros 5.57.0 → 5.58.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.js CHANGED
@@ -1136,6 +1136,73 @@ automation
1136
1136
  if (!options.write && result.conversions) console.log('Re-run with --write to apply.')
1137
1137
  })
1138
1138
 
1139
+ /**
1140
+ * @description Report hand-written property values in prose ("defaults to
1141
+ * 128 MiB", "the minimum is 1 minute") next to a property mention, check each
1142
+ * against the property data for the repo's own version (selected exactly as
1143
+ * the prop: macro selects it), and propose a propval: replacement. Mismatches
1144
+ * are likely doc errors. Also reads example command output in listing
1145
+ * blocks (rpk topic describe, rpk cluster config get and export, Admin API
1146
+ * cluster_config, kafka-configs --describe, Key/Value/Source records) and
1147
+ * reports a row that asserts the default, such as SOURCE DEFAULT_CONFIG, but
1148
+ * shows another value as stale example output, listed apart from prose
1149
+ * findings. Dry run by default, writing a JSON and a Markdown report; --write
1150
+ * applies only exact matches stated in the same sentence as their property,
1151
+ * never a mismatch, a table cell or example output. Skips prose inside code
1152
+ * blocks and generated content.
1153
+ *
1154
+ * @example
1155
+ * # Report on a docs checkout
1156
+ * npx doc-tools generate migrate-property-values --docs-dir . --report-dir ./propval-report
1157
+ *
1158
+ * # Report on cloud-docs, which borrows the self-managed docs' property data
1159
+ * npx doc-tools generate migrate-property-values --docs-dir ../cloud-docs --lender ../docs
1160
+ *
1161
+ * # Apply the auto-safe exact matches
1162
+ * npx doc-tools generate migrate-property-values --docs-dir . --write
1163
+ */
1164
+ automation
1165
+ .command('migrate-property-values')
1166
+ .description('Report hand-written property values in prose and in example command output, check them against the property data, and propose propval: macros. Dry run unless --write is given; --write applies exact same-sentence prose matches only, never example output.')
1167
+ .option('--docs-dir <path>', 'Docs repo root containing antora.yml and modules/', '.')
1168
+ .option('--lender <path...>', 'Other component checkouts whose property data the repo borrows (cloud-docs borrows the docs repo\'s)')
1169
+ .option('--report-dir <path>', 'Where to write property-values-report.json and property-values-report.md', '.')
1170
+ .option('--write', 'Replace exact, same-sentence matches with propval: macros (mismatches are never rewritten)')
1171
+ .action((options) => {
1172
+ const { scanRepo, renderMarkdown } = require('../tools/migrate-property-values.js')
1173
+ const result = scanRepo({
1174
+ docsDir: path.resolve(options.docsDir),
1175
+ lenderDirs: (options.lender || []).map((dir) => path.resolve(dir)),
1176
+ write: Boolean(options.write),
1177
+ })
1178
+ if (result.error) {
1179
+ console.error(`Error: ${result.error}`)
1180
+ process.exit(1)
1181
+ }
1182
+ result.label = `${result.component}@${result.version || 'unversioned'}`
1183
+ const reportDir = path.resolve(options.reportDir)
1184
+ fs.mkdirSync(reportDir, { recursive: true })
1185
+ fs.writeFileSync(path.join(reportDir, 'property-values-report.json'), JSON.stringify(result, null, 2) + '\n')
1186
+ fs.writeFileSync(path.join(reportDir, 'property-values-report.md'), renderMarkdown([result]))
1187
+ const { counts } = result
1188
+ console.log(`Checked against ${result.dataset} (${result.surface}).`)
1189
+ console.log(`${result.findings.length} value statement(s) in ${result.files} files: ${counts.mismatch} mismatch, ${counts.unset} unset, ${counts.exact} exact (${counts.autoSafe} auto-safe), ${counts.equivalent} equivalent, ${counts.unverifiable} unverifiable.`)
1190
+ for (const f of result.findings.filter((x) => x.status === 'mismatch')) {
1191
+ console.log(` MISMATCH ${f.file}:${f.line} ${f.property} ${f.field}: written ${f.written}, data ${f.dataValue}`)
1192
+ }
1193
+ for (const f of result.findings.filter((x) => x.status === 'unset')) {
1194
+ console.log(` UNSET ${f.file}:${f.line} ${f.property} ${f.field}: ${f.note}`)
1195
+ }
1196
+ const examples = result.findings.filter((x) => x.status === 'example-output')
1197
+ console.log(`${examples.length} value(s) in example output: ${counts.exampleStale} stale, ${examples.filter((x) => x.verdict === 'matches').length} matching, ${examples.filter((x) => x.verdict === 'unverifiable').length} unverifiable (never written).`)
1198
+ for (const f of examples.filter((x) => x.verdict === 'stale')) {
1199
+ console.log(` EXAMPLE ${f.file}:${f.line} ${f.property}${f.clusterProperty ? ` (${f.clusterProperty})` : ''}: shown ${f.written}${f.source ? ` ${f.source}` : ''}, default ${f.dataRaw || f.dataValue}`)
1200
+ }
1201
+ if (options.write) console.log(`Replaced auto-safe exact matches in ${result.changedFiles} file(s).`)
1202
+ else if (counts.autoSafe) console.log('Re-run with --write to replace the auto-safe exact matches.')
1203
+ console.log(`Report: ${path.join(reportDir, 'property-values-report.md')}`)
1204
+ })
1205
+
1139
1206
  /**
1140
1207
  * generate property-docs
1141
1208
  *
@@ -0,0 +1,255 @@
1
+ 'use strict'
2
+
3
+ /* Field values for one entry of a published redpanda-properties-<tag>.json,
4
+ * formatted the way the generated property reference renders them.
5
+ *
6
+ * Shared by the propval: inline macro (macros/prop.js) and the
7
+ * migrate-property-values report, so prose, the migration report, and the
8
+ * reference tables agree. The value strings come from the reference
9
+ * generator's own helpers wherever the tables show the field:
10
+ *
11
+ * default formatPropertyValue (the table's code value), and the
12
+ * extractor's default_human_readable where the data carries it
13
+ * (the table's parenthetical, such as "2 weeks").
14
+ * unit the table's Unit row: the units field, else formatUnits(name).
15
+ *
16
+ * Two humanizations go beyond what the table prints, and both are derived
17
+ * only from data the table also shows, never guessed:
18
+ *
19
+ * Durations without default_human_readable use humanizeDuration, a port of
20
+ * the extractor's format_time_human_readable (the function that produced
21
+ * default_human_readable), so the wording is identical.
22
+ * Byte values use IEC units (128 MiB) only when the table's Unit row says
23
+ * bytes and the value is an exact multiple. Anything else renders exactly.
24
+ */
25
+
26
+ const formatPropertyValue = require('../tools/property-extractor/helpers/formatPropertyValue')
27
+ const formatUnits = require('../tools/property-extractor/helpers/formatUnits')
28
+ const { escapeHtml } = require('./html-utils')
29
+
30
+ const FORMAT_HUMAN = 'human'
31
+ const FORMAT_RAW = 'raw'
32
+
33
+ // Every field propval: supports. `cloudHidden` marks the fields the reference
34
+ // pages wrap in ifndef::env-cloud[], so a Cloud page never states them either.
35
+ const FIELDS = {
36
+ default: { cloudHidden: true },
37
+ type: {},
38
+ unit: {},
39
+ min: {},
40
+ max: {},
41
+ restart: {},
42
+ nullable: {},
43
+ values: {},
44
+ scope: {},
45
+ introduced: { cloudHidden: true },
46
+ visibility: { cloudHidden: true },
47
+ restored: { cloudHidden: true },
48
+ aliases: { cloudHidden: true },
49
+ 'cloud-access': {},
50
+ }
51
+
52
+ // The JSON key names and table labels are accepted as aliases, so a writer
53
+ // who types what they see in the data or on the page gets the same field.
54
+ const FIELD_ALIASES = {
55
+ minimum: 'min',
56
+ maximum: 'max',
57
+ needs_restart: 'restart',
58
+ 'requires-restart': 'restart',
59
+ units: 'unit',
60
+ enum: 'values',
61
+ 'accepted-values': 'values',
62
+ config_scope: 'scope',
63
+ version: 'introduced',
64
+ gets_restored: 'restored',
65
+ cloud: 'cloud-access',
66
+ }
67
+
68
+ const DURATION_UNITS = { milliseconds: 1, seconds: 1000 }
69
+
70
+ /**
71
+ * Canonical field name, or undefined for a field propval: does not support.
72
+ */
73
+ function normalizeField (field) {
74
+ const key = String(field == null ? '' : field).trim().toLowerCase()
75
+ if (Object.prototype.hasOwnProperty.call(FIELDS, key)) return key
76
+ if (Object.prototype.hasOwnProperty.call(FIELD_ALIASES, key)) return FIELD_ALIASES[key]
77
+ return undefined
78
+ }
79
+
80
+ /**
81
+ * The unit the reference table prints for a property, lowercased for prose:
82
+ * the units field, else the one formatUnits derives from the name suffix.
83
+ * Empty when the table prints no Unit row.
84
+ */
85
+ function unitOf (entry) {
86
+ if (entry && typeof entry.units === 'string' && entry.units.trim()) return entry.units.trim()
87
+ return formatUnits(entry && entry.name) || ''
88
+ }
89
+
90
+ /**
91
+ * Port of property_extractor.py's format_time_human_readable: the largest
92
+ * unit that divides the value evenly, else the value in its own unit.
93
+ *
94
+ * @param {number} value
95
+ * @param {'milliseconds'|'seconds'} unit
96
+ */
97
+ function humanizeDuration (value, unit) {
98
+ const ms = unit === 'seconds' ? value * 1000 : value
99
+ const units = [
100
+ [365 * 24 * 60 * 60 * 1000, 'year', 'years'],
101
+ [7 * 24 * 60 * 60 * 1000, 'week', 'weeks'],
102
+ [24 * 60 * 60 * 1000, 'day', 'days'],
103
+ [60 * 60 * 1000, 'hour', 'hours'],
104
+ [60 * 1000, 'minute', 'minutes'],
105
+ [1000, 'second', 'seconds'],
106
+ [1, 'millisecond', 'milliseconds'],
107
+ ]
108
+ for (const [threshold, singular, plural] of units) {
109
+ if (ms >= threshold && ms % threshold === 0) {
110
+ const count = ms / threshold
111
+ return `${count} ${count === 1 ? singular : plural}`
112
+ }
113
+ }
114
+ return unit === 'seconds' ? `${value} seconds` : `${value} milliseconds`
115
+ }
116
+
117
+ /**
118
+ * IEC rendering of a byte count, only for exact multiples: 134217728 is
119
+ * "128 MiB", 1000 stays "1000 bytes".
120
+ */
121
+ function humanizeBytes (value) {
122
+ if (value === 0) return '0 bytes'
123
+ const steps = [
124
+ [1024 ** 4, 'TiB'],
125
+ [1024 ** 3, 'GiB'],
126
+ [1024 ** 2, 'MiB'],
127
+ [1024, 'KiB'],
128
+ ]
129
+ for (const [size, label] of steps) {
130
+ if (value >= size && value % size === 0) return `${value / size} ${label}`
131
+ }
132
+ return `${value} ${value === 1 ? 'byte' : 'bytes'}`
133
+ }
134
+
135
+ /**
136
+ * A number in the property's own unit, humanized when the unit is known and
137
+ * the value is a non-negative integer. Negative values are sentinels (-1 for
138
+ * "unlimited") and render as written.
139
+ */
140
+ function humanizeNumber (value, unit) {
141
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) return String(value)
142
+ if (DURATION_UNITS[unit]) return humanizeDuration(value, unit)
143
+ if (unit === 'bytes') return humanizeBytes(value)
144
+ return String(value)
145
+ }
146
+
147
+ // An integer outside JavaScript's exact range (2^64 - 1 sentinels such as
148
+ // virtual_cluster_min_producer_ids) has already lost digits by the time
149
+ // JSON.parse returns it, so there is no exact value left to render.
150
+ const isImprecise = (value) => typeof value === 'number' && Number.isInteger(value) && !Number.isSafeInteger(value)
151
+ const imprecise = () => ({ ok: false, reason: 'imprecise' })
152
+ const plain = (text) => ({ ok: true, text: String(text) })
153
+ const missing = () => ({ ok: false, reason: 'missing' })
154
+ const yesNo = (value, format) => plain(format === FORMAT_RAW ? String(value) : (value ? 'yes' : 'no'))
155
+
156
+ /**
157
+ * The value of one field of one property entry.
158
+ *
159
+ * @param {object} entry - One value of the JSON's properties object.
160
+ * @param {string} field - A field name or alias.
161
+ * @param {object} [opts]
162
+ * @param {'human'|'raw'} [opts.format] - human (default) or the exact value.
163
+ * @returns {{ok: true, text: string, html?: string} |
164
+ * {ok: false, reason: 'unsupported'|'missing'|'imprecise'}}
165
+ * html, when present, is pre-escaped markup to use instead of text.
166
+ */
167
+ function fieldValue (entry, field, { format = FORMAT_HUMAN } = {}) {
168
+ const name = normalizeField(field)
169
+ if (!name) return { ok: false, reason: 'unsupported' }
170
+ const has = (key) => Object.prototype.hasOwnProperty.call(entry, key) && entry[key] !== undefined
171
+ const raw = format === FORMAT_RAW
172
+
173
+ switch (name) {
174
+ case 'default': {
175
+ if (!has('default')) return missing()
176
+ const value = entry.default
177
+ if (isImprecise(value)) return imprecise()
178
+ const exact = String(formatPropertyValue(value, entry.type))
179
+ if (raw) return plain(exact)
180
+ if (typeof entry.default_human_readable === 'string' && entry.default_human_readable.trim()) {
181
+ return plain(entry.default_human_readable.trim())
182
+ }
183
+ if (typeof value === 'number') return plain(humanizeNumber(value, unitOf(entry)))
184
+ return plain(exact)
185
+ }
186
+ case 'type':
187
+ return has('type') && entry.type !== '' ? plain(entry.type) : missing()
188
+ case 'unit': {
189
+ const unit = unitOf(entry)
190
+ return unit ? plain(unit) : missing()
191
+ }
192
+ case 'min':
193
+ case 'max': {
194
+ const key = name === 'min' ? 'minimum' : 'maximum'
195
+ if (!has(key) || entry[key] === null) return missing()
196
+ if (isImprecise(entry[key])) return imprecise()
197
+ return plain(raw ? String(entry[key]) : humanizeNumber(entry[key], unitOf(entry)))
198
+ }
199
+ case 'restart':
200
+ return has('needs_restart') && typeof entry.needs_restart === 'boolean' ? yesNo(entry.needs_restart, format) : missing()
201
+ case 'nullable':
202
+ return has('nullable') && typeof entry.nullable === 'boolean' ? yesNo(entry.nullable, format) : missing()
203
+ case 'restored':
204
+ return has('gets_restored') && typeof entry.gets_restored === 'boolean' ? yesNo(entry.gets_restored, format) : missing()
205
+ case 'values': {
206
+ const list = Array.isArray(entry.enum) && entry.enum.length
207
+ ? entry.enum
208
+ : entry.items && Array.isArray(entry.items.enum) && entry.items.enum.length ? entry.items.enum : undefined
209
+ if (!list) return missing()
210
+ if (raw) return plain(JSON.stringify(list))
211
+ // Each value in its own code element, as the table prints them: some
212
+ // values contain commas (compact,delete), so a plain comma-joined list
213
+ // would be ambiguous.
214
+ return {
215
+ ok: true,
216
+ text: list.map(String).join(', '),
217
+ html: list.map((v) => `<code>${escapeHtml(String(v))}</code>`).join(', '),
218
+ }
219
+ }
220
+ case 'scope':
221
+ return has('config_scope') && entry.config_scope ? plain(entry.config_scope) : missing()
222
+ case 'introduced':
223
+ return has('version') && entry.version ? plain(entry.version) : missing()
224
+ case 'visibility':
225
+ return has('visibility') && entry.visibility ? plain(entry.visibility) : missing()
226
+ case 'aliases':
227
+ return Array.isArray(entry.aliases) && entry.aliases.length
228
+ ? plain(raw ? JSON.stringify(entry.aliases) : entry.aliases.join(', '))
229
+ : missing()
230
+ case 'cloud-access': {
231
+ // cloud_* fields describe cluster configuration only (see isAvailable
232
+ // in macros/prop.js), so other scopes have no Cloud access value.
233
+ if (entry.config_scope !== 'cluster') return missing()
234
+ if (entry.cloud_readonly === true) return plain(raw ? 'readonly' : 'read-only')
235
+ if (entry.cloud_editable === true) return plain('editable')
236
+ if (entry.cloud_supported === false) return plain(raw ? 'unsupported' : 'not available')
237
+ return missing()
238
+ }
239
+ default:
240
+ return { ok: false, reason: 'unsupported' }
241
+ }
242
+ }
243
+
244
+ module.exports = {
245
+ FIELDS,
246
+ FIELD_ALIASES,
247
+ FORMAT_HUMAN,
248
+ FORMAT_RAW,
249
+ normalizeField,
250
+ fieldValue,
251
+ unitOf,
252
+ humanizeDuration,
253
+ humanizeBytes,
254
+ humanizeNumber,
255
+ }
@@ -35,6 +35,29 @@ Every release series has its own copy of that data, and the build picks the copy
35
35
 
36
36
  *Once per paragraph is enough.* Mark the first mention of a property in a paragraph and leave later mentions as plain backticks -- a paragraph doesn't need three identical tooltips. The migration tool follows this rule, and the UI enforces it as a backstop: even if every mention is marked, only the first one per paragraph gets the tooltip.
37
37
 
38
+ === Property values: the propval macro
39
+
40
+ `prop:` marks a property's *name*. Prose often states its *value* too: "defaults to 128 MiB", "the minimum is one minute". Typed by hand, those drift when a release changes a default, and nothing notices. A real example: the disk space page said `log_segment_size` defaults to 1 GiB long after the default became 128 MiB.
41
+
42
+ `propval:` renders the value from the same property data instead:
43
+
44
+ [,asciidoc]
45
+ ----
46
+ Set prop:log_segment_ms[], which defaults to propval:log_segment_ms[default].
47
+ ----
48
+
49
+ That renders "defaults to 2 weeks". Other fields are `type`, `unit`, `min`, `max`, `restart`, `nullable`, `values`, `scope`, `introduced`, `visibility`, `restored`, `aliases`, and `cloud-access`, and `format=raw` gives the exact value (`1209600000`). The full list is in link:REFERENCE.adoc#propval[the macros reference].
50
+
51
+ *It reads the same data as prop:, chosen the same way.* A 25.3 page gets 25.3's value, a beta page gets the newest release candidate's, and a Cloud page gets the newest GA value. The code is shared, not copied, so the two macros can't drift apart.
52
+
53
+ *It agrees with the reference tables.* Values are formatted by the same helpers that render the property reference pages, so "2 weeks" in prose and "2 weeks" in the Default cell come from one place. Byte counts read as `128 MiB` only where the reference says the unit is bytes; where the data gives no unit, the exact number renders rather than a guessed unit.
54
+
55
+ *It never guesses.* When the build can't verify a value (no data for the page's version, an unknown property, a field the property doesn't have, a property Cloud readers can't set), the page shows a visible `<unknown default>` and the build log says why. A wrong number is worse than an obvious gap.
56
+
57
+ *Cloud pages state fewer values.* The Cloud property reference shows no defaults, because Cloud configures them, so `propval:x[default]` on a Cloud page renders the placeholder with a warning. Keep default values in `ifndef::env-cloud[]` content, or use `cloud-access` to say whether a Cloud user can change the property.
58
+
59
+ To find the hand-written values already in a repo, run `doc-tools generate migrate-property-values`. It reports every value stated next to a property mention, compares it with the data for that repo's version, and flags mismatches, which are usually real doc errors. It also reports stale example output, such as an `rpk topic describe` row with source `DEFAULT_CONFIG` that shows an old default. `--write` replaces only exact matches, never a mismatch or example output.
60
+
38
61
  === How "the build works out the page" (dynamic discovery)
39
62
 
40
63
  Rather than hardcoding "cluster properties live on the cluster-properties page," the macro reads the actual docs during the build. Every documented property appears as a heading in the generated property files, and each reference page pulls in one or more of those files. The macro follows that chain -- which pages exist, what they include, which property headings end up where -- and builds a lookup table per doc set.
@@ -106,7 +129,8 @@ Cloud is different from Kubernetes here, not just another context to adapt to. R
106
129
 
107
130
  * The extensions preview site's test page demonstrates every form of both macros, including intentional failure cases, and a Kubernetes-environment page shows the environment-specific behavior side by side.
108
131
  * The docs-ui preview's `property-tooltips-test` and `enterprise-macro-test` pages show every output shape the UI styles, including the negative cases (unmarked names must get nothing).
132
+ * The extensions preview site's `property-values-test` page shows every `propval:` field, both formats, and each placeholder case.
109
133
 
110
134
  == The one-sentence version
111
135
 
112
- Nothing in the docs claims a name is a property or a feature is enterprise unless a writer said so explicitly *and* the build verified it against the source of truth.
136
+ Nothing in the docs claims a name is a property, states a property's value, or marks a feature enterprise unless a writer said so explicitly *and* the build verified it against the source of truth.
@@ -58,7 +58,9 @@ IMPORTANT: Macros must be registered under the `asciidoc.extensions` key in your
58
58
  === Inline macros
59
59
 
60
60
  * **glossterm** - Reference glossary terms with tooltips
61
- * **config_ref** - Link to configuration properties
61
+ * **prop** - Reference a configuration property, with a validated tooltip and optional link
62
+ * **propval** - Render a property's default, minimum, unit, or other value from the property data
63
+ * **config_ref** - Link to configuration properties (deprecated; use prop)
62
64
  * **helm_ref** - Link to Helm chart values
63
65
  * **badge** - Display status badges
64
66
 
@@ -223,7 +223,7 @@ Registered together with the `enterprise` inline macro.
223
223
 
224
224
  This inline macro references a Redpanda configuration property in prose. The property renders as a marked `<code>` element (`property-ref` class plus a `data-property-name` attribute) that the docs UI decorates with a hover-documentation tooltip. Marking is opt-in: only properties referenced through this macro get tooltips, so ambiguous words such as `admin` or `rack` in unrelated contexts (Helm values, feature settings) are never decorated by mistake.
225
225
 
226
- The macro checks that a property is actually available to the audience reading the page, and links it only where a page documents it. It validates against the published property data: a `redpanda-properties-<tag>.json` attachment in the `reference` module, the same data the tooltips fetch at runtime. See <<prop-availability>> for which properties each doc set may reference and <<prop-version-selection>> for which file a given page uses. To migrate existing plain-backtick property mentions, use `doc-tools generate migrate-property-refs`.
226
+ The macro checks that a property is actually available to the audience reading the page, and links it only where a page documents it. It validates against the published property data: a `redpanda-properties-<tag>.json` attachment in the `reference` module, the same data the tooltips fetch at runtime. See <<prop-availability>> for which properties each doc set may reference and <<prop-version-selection>> for which file a given page uses. To migrate existing plain-backtick property mentions, use `doc-tools generate migrate-property-refs`. To render a property's value (its default, minimum, unit, and so on) rather than its name, use <<propval>>.
227
227
 
228
228
  === Usage
229
229
 
@@ -331,6 +331,163 @@ asciidoc:
331
331
  - '@redpanda-data/docs-extensions-and-macros/macros/prop'
332
332
  ----
333
333
 
334
+ == propval
335
+
336
+ This inline macro renders one value from the property data as plain text, so a sentence such as "defaults to 2 weeks" is generated from the same data as the property reference instead of typed by hand. Hand-typed values drift when a release changes a default, and nothing catches it.
337
+
338
+ [,asciidoc]
339
+ ----
340
+ Set prop:log_segment_ms[], which defaults to propval:log_segment_ms[default].
341
+ The minimum is propval:log_segment_ms[min].
342
+ Changing prop:cloud_storage_enabled[] requires a restart: propval:cloud_storage_enabled[restart].
343
+ The exact default is `propval:log_segment_ms[default,format=raw]`.
344
+ ----
345
+
346
+ Rendered against `redpanda-properties-v26.2.4.json`, those lines read: "defaults to 2 weeks", "The minimum is 1 minute", "requires a restart: yes", and "The exact default is `1209600000`".
347
+
348
+ `propval:` is registered by the `prop` macro's extension, so a playbook that registers `macros/prop` has both. It shares every data decision with `prop:` rather than copying them: the same dataset for the page's version (<<prop-version-selection>>), the same `property-validate` modes, the same Cloud availability gate (<<prop-availability>>), and the same refusal to use a neighbouring release's data when a version has none.
349
+
350
+ === Usage
351
+
352
+ The target is the property name. The first positional attribute (or `field=`) names the field, and defaults to `default`.
353
+
354
+ [cols="1,2,2"]
355
+ |===
356
+ | Field | Source in the property JSON | Example (v26.2.4)
357
+
358
+ | `default`
359
+ | `default`, formatted as the reference table formats it. The human-readable form is the table's parenthetical (`default_human_readable`) when the data has one.
360
+ | `log_segment_ms`: `2 weeks`; `fetch_max_bytes`: `55 MiB`; `cloud_storage_enabled`: `false`; `node_id`: `null`
361
+
362
+ | `type`
363
+ | `type`
364
+ | `integer`
365
+
366
+ | `unit`
367
+ | `units`, else the unit the table derives from the name suffix (`_ms`, `_bytes`, and so on)
368
+ | `milliseconds`
369
+
370
+ | `min`, `max`
371
+ | `minimum`, `maximum`, humanized with the unit
372
+ | `log_segment_ms` min: `1 minute`
373
+
374
+ | `restart`
375
+ | `needs_restart`
376
+ | `yes` or `no`
377
+
378
+ | `nullable`
379
+ | `nullable`
380
+ | `yes` or `no`
381
+
382
+ | `values`
383
+ | `enum` (or `items.enum` for arrays), each value as inline code, because some values contain commas
384
+ | `none`, `delete`, `compact`, `compact,delete`
385
+
386
+ | `scope`
387
+ | `config_scope`
388
+ | `cluster`, `broker`, or `topic`
389
+
390
+ | `introduced`
391
+ | `version`
392
+ | `v26.1.1`
393
+
394
+ | `visibility`
395
+ | `visibility`
396
+ | `user`, `tunable`
397
+
398
+ | `restored`
399
+ | `gets_restored` (restored on Whole Cluster Restore)
400
+ | `yes` or `no`
401
+
402
+ | `aliases`
403
+ | `aliases`
404
+ | comma-separated names
405
+
406
+ | `cloud-access`
407
+ | `cloud_editable` and `cloud_readonly`, for cluster properties only
408
+ | `editable`, `read-only`, or `not available`
409
+ |===
410
+
411
+ The JSON key names and table labels also work as field names: `minimum`, `maximum`, `needs_restart`, `units`, `enum`, `config_scope`, `version`, `gets_restored`.
412
+
413
+ format::
414
+ `human` (default) or `raw`. `raw` gives the exact value: the code value in the reference table for `default`, the plain number for `min` and `max`, `true` or `false` for the yes-or-no fields, and a JSON array for `values` and `aliases`.
415
+
416
+ The output is a `<span>` with the stable class `property-value` and `data-property` and `data-field` attributes. It is plain text with no tooltip; mark the name with `prop:` for that. The macro works inside backticks, so `` `propval:x[default,format=raw]` `` renders as inline code. Don't put `values` inside backticks: it already renders each value as inline code, and backticks would nest one code element inside another.
417
+
418
+ === Formatting
419
+
420
+ Values come from the reference generator's own helpers, so prose and the reference tables cannot disagree:
421
+
422
+ * `default` uses `formatPropertyValue`, the helper that renders the table's Default cell. In the human form, a duration uses the table's parenthetical when the data has one.
423
+ * A duration with no parenthetical in the data is humanized by a port of the extractor's `format_time_human_readable`, the function that produced the parenthetical: the largest unit that divides the value evenly, else the value in its own unit (`1500 milliseconds`).
424
+ * A byte count renders in IEC units (`128 MiB`) only when the table's Unit row says bytes and the value is an exact multiple. Otherwise the exact number renders. `log_segment_size` and `segment.bytes` have no unit in the data or the table, so they render `134217728`. Adding `units` for them belongs in the property extractor, not in prose.
425
+ * Negative numbers are sentinels (`-1` for unlimited) and render as written.
426
+ * `null`, an empty string, and booleans render as the table renders them: `null`, `null`, `true`, `false`.
427
+ * A topic property that the data leaves unset, such as `retention.bytes`, renders `null` for `default`, as the table does. The topic inherits its cluster property (`corresponding_cluster_property` in the data), so in prose, point to that property with `prop:`, such as "inherits prop:retention_bytes[]", rather than saying the default is `null`.
428
+
429
+ === Failures
430
+
431
+ A value has no safe fallback, so whenever the build cannot verify one, `propval:` renders a visible placeholder such as `<unknown default>` (class `property-value property-value-unknown`) and logs why, naming the property, the field, and the dataset. It never renders a guessed value. The cases:
432
+
433
+ * The page's version has no property data, or the component has none (the same diagnostic `prop:` logs, plus one for the `propval:` call).
434
+ * The property is not in the page's dataset.
435
+ * The property is not available on a Cloud page (`cloud_supported: false` on a cluster property).
436
+ * The field is not one `propval:` supports.
437
+ * The property has no such field in the data, for example `max` on a property with no maximum, or `default` on a deprecated property the data gives no default.
438
+ * The value is an integer beyond JavaScript's exact range (the 2^64^-1 sentinel of `virtual_cluster_min_producer_ids`), which `JSON.parse` has already rounded.
439
+ * On a Cloud page, the field is one the Cloud property reference does not show: `default`, `introduced`, `visibility`, `restored`, and `aliases`. The reference renders those only outside `env-cloud` (Cloud shows "Available in the Redpanda Cloud Console" in the Default row), so a Cloud page cannot state them either. Put the sentence in an `ifndef::env-cloud[]` block, or use a field the Cloud reference shows, such as `cloud-access`.
440
+
441
+ With `property-validate` set to `error`, an unknown property, an unsupported field, a field the property does not have, a property not available on Cloud, or a field the Cloud reference does not show fails the build. The last two appear only in the Cloud build, so cloud-docs needs `property-validate: error` to catch single-sourced prose that would render a placeholder. With `off`, the placeholder is still rendered and still reported, because `off` means "do not check names", and a silent `<unknown default>` would reach readers unnoticed.
442
+
443
+ === Migrating hand-written values
444
+
445
+ `doc-tools generate migrate-property-values` finds hand-written values next to a property mention ("defaults to X", "the default is X", "default: X", "minimum X", "is X by default") and checks each against the data for the repo's own version, selected exactly as `prop:` selects it. It writes a JSON and a Markdown report. Each finding is one of:
446
+
447
+ exact:: The written text is what `propval:` renders. Auto-safe when the statement is in the same sentence as its property, no other property is named in that paragraph line or its list entry, the data value is not null, and the file is not release or upgrade notes. On a self-managed page, a `default` (or another field the Cloud reference hides), or any field of a property Cloud does not support, is auto-safe only inside `ifndef::env-cloud[]` or in a page (not a partial) with no `tag::` regions. Otherwise the page may be single-sourced into cloud-docs, where `propval:` renders a placeholder, so the finding is proposed only: wrap the sentence in `ifndef::env-cloud[]` first.
448
+ equivalent:: Same value, different wording (`7 days` and `1 week`). Proposed, never applied.
449
+ mismatch:: The written value differs from the data. Likely a doc error. Never applied.
450
+ unset:: The data default is null (not set), but the prose states a value. Redpanda often computes a fallback at runtime for an unset property, such as 10% of free disk for `compaction_ctrl_backlog_size`, so confirm the value in the source. Never applied.
451
+ unverifiable:: Not comparable: unparseable, a field the property does not have, a sentence qualified to another audience or release ("In Redpanda Cloud", "in earlier versions", "If the default is"), content inside `ifdef::env-cloud[]` on a self-managed page, a Cloud default, or "no default" for a topic property, which inherits its cluster property.
452
+ example-output:: A value shown in command output inside a listing, literal, or fenced block. These findings are counted and listed apart from prose findings, and never applied. Each has a verdict: `stale`, `matches`, or `unverifiable` with a reason.
453
+
454
+ Tables are scanned too. A statement in a cell belongs to the property that opens the row or cell, and a `Default` column states the default of the row's property. Table findings are reported but never applied.
455
+
456
+ `--write` applies only auto-safe exact matches. The tool skips prose inside code blocks, headings, attribute entries, comment lines, `ifndef::env-cloud[]` content on a Cloud page, and generated content. For cloud-docs, pass the docs checkout with `--lender` so Cloud can borrow its GA data.
457
+
458
+ [,bash]
459
+ ----
460
+ npx doc-tools generate migrate-property-values --docs-dir . --report-dir ./propval-report
461
+ npx doc-tools generate migrate-property-values --docs-dir ../cloud-docs --lender . --report-dir ./propval-report-cloud
462
+ ----
463
+
464
+ ==== Example output
465
+
466
+ The tool reads these output shapes: `rpk topic describe` config tables (`KEY VALUE SOURCE`), `name=value (SOURCE)` lines, `kafka-configs --describe` lines (the first synonym's source), Key/Value/Source records in JSON or YAML (debug bundles, Topic resource status), `rpk cluster config get` output in the same block or an Input/Output pair, `rpk cluster config export` YAML, and Admin API `/v1/cluster_config` JSON. `rpk cluster config status` output states no values. A topic property is compared with the cluster property it inherits, taken from the data's `corresponding_cluster_property`, or `alternate_cluster_property` when the same output shows `cleanup.policy` set to compact. A Kafka broker alias such as `log.segment.bytes` is not in the data, so the Redpanda-named synonym row is compared. Values are compared exactly as raw bytes, milliseconds, booleans, or enum text. `-1` matches a limit that the data leaves unset.
467
+
468
+ A row is compared only when it asserts the default. A row with source `DEFAULT_CONFIG` or `DEFAULT` asserts it. Output with no source column (`config get`, `export`, Admin API) asserts it only when the text just before the block mentions the default. The following are `unverifiable`, with the first matching reason:
469
+
470
+ * `not-default-source`: Another source, such as `DYNAMIC_TOPIC_CONFIG` or `STATIC_BROKER_CONFIG`.
471
+ * `ifdef::env-cloud` on a self-managed page, or `cloud-default` on a Cloud page.
472
+ * `set-earlier-on-page`: The page sets the cluster property before the block, as `name=value`, `name: value` (including Helm `config.cluster.name`), or `rpk cluster config set name value`. A topic-level override doesn't count, because it would change the row's source.
473
+ * `helm-operator-context`: A Kubernetes page (`kubernetes/` path or `k-` file name), or a command run through `kubectl` or `helm`. The chart or operator may set the value.
474
+ * `dev-or-bootstrap-config`: The page starts Redpanda in `dev-container` mode, with `rpk container start`, or from a bootstrap file.
475
+ * `current-value`: Output with no source column, and no mention of the default before it.
476
+ * `unset-in-data` or `not-comparable`.
477
+
478
+ The report lists stale examples in their own table. It lists unverifiable rows whose value differs from the default in a context that may set them (other than `not-default-source`) for a human to check.
479
+
480
+ === Registration
481
+
482
+ Registered by the `prop` macro's extension:
483
+
484
+ [,yaml]
485
+ ----
486
+ asciidoc:
487
+ extensions:
488
+ - '@redpanda-data/docs-extensions-and-macros/macros/prop'
489
+ ----
490
+
334
491
  == glossterm
335
492
 
336
493
  The `glossterm` inline macro provides a way to define and reference glossary terms in your AsciiDoc documents.
@@ -25,6 +25,7 @@ asciidoc:
25
25
  extensions:
26
26
  - '@redpanda-data/docs-extensions-and-macros/macros/glossary'
27
27
  - '@redpanda-data/docs-extensions-and-macros/macros/config-ref'
28
+ - '@redpanda-data/docs-extensions-and-macros/macros/prop'
28
29
  - '@redpanda-data/docs-extensions-and-macros/macros/helm-ref'
29
30
  ----
30
31
 
@@ -63,6 +64,33 @@ config_ref:cleanup.policy,false[]
63
64
  * `isLink` - Whether to create a link (true/false, required)
64
65
  * `path` - Document path for link (required if isLink is true)
65
66
 
67
+ === prop and propval - Property names and values
68
+
69
+ Mark a configuration property's name, and render its values from the property data:
70
+
71
+ [,asciidoc]
72
+ ----
73
+ Set prop:log_segment_ms[], which defaults to propval:log_segment_ms[default].
74
+ The minimum is propval:log_segment_ms[min].
75
+ ----
76
+
77
+ With Redpanda 26.2 data, that renders "defaults to 2 weeks" and "The minimum is 1 minute".
78
+
79
+ *Syntax:* `prop:name[]` and `propval:name[field,format=raw]`
80
+
81
+ * `name` - Property name (required)
82
+ * `field` - `default` (when omitted), `type`, `unit`, `min`, `max`, `restart`, `nullable`, `values`, `scope`, `introduced`, `visibility`, `restored`, `aliases`, or `cloud-access`
83
+ * `format=raw` - The exact value (`1209600000`) instead of the human-readable one (`2 weeks`)
84
+
85
+ Register `@redpanda-data/docs-extensions-and-macros/macros/prop`, which provides both macros. If a value can't be verified for the page's version, the page shows `<unknown default>` and the build log says why. See link:REFERENCE.adoc#propval[the macros reference] for every field and failure case.
86
+
87
+ To find hand-written values in a repo and check them against the data:
88
+
89
+ [,bash]
90
+ ----
91
+ npx doc-tools generate migrate-property-values --docs-dir . --report-dir ./propval-report
92
+ ----
93
+
66
94
  === helm_ref - Helm value references
67
95
 
68
96
  Link to Helm chart values: