@redpanda-data/docs-extensions-and-macros 5.46.0 → 5.47.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 +15 -2
- package/cli-utils/sync-schemas.js +66 -12
- package/docs-data/property-overrides.schema.json +202 -29
- package/package.json +1 -1
- package/tools/overrides-audit/adapters/properties.js +14 -3
- package/tools/overrides-audit/classify.js +175 -3
- package/tools/property-extractor/README.adoc +141 -2
- package/tools/property-extractor/compare-properties.js +12 -2
- package/tools/property-extractor/generate-handlebars-docs.js +262 -11
- package/tools/property-extractor/helpers/applyPropertyLinks.js +586 -0
- package/tools/property-extractor/helpers/audienceScope.js +172 -0
- package/tools/property-extractor/helpers/findAudienceScopeProblems.js +121 -0
- package/tools/property-extractor/helpers/index.js +3 -0
- package/tools/property-extractor/helpers/seeAlsoView.js +10 -24
- package/tools/property-extractor/property_extractor.py +30 -2
- package/tools/property-extractor/templates/property.hbs +32 -2
- package/tools/property-extractor/templates/topic-property.hbs +32 -2
|
@@ -59,6 +59,10 @@ const PRODUCT_ATTR_ALLOWLIST = Object.freeze([])
|
|
|
59
59
|
*/
|
|
60
60
|
const KEEP_BY_DESIGN_FIELDS = Object.freeze([
|
|
61
61
|
'related_topics',
|
|
62
|
+
'see_also',
|
|
63
|
+
'admonitions',
|
|
64
|
+
'links',
|
|
65
|
+
'includes',
|
|
62
66
|
'category',
|
|
63
67
|
'config_scope',
|
|
64
68
|
'version',
|
|
@@ -89,7 +93,59 @@ const META_FIELDS = Object.freeze(['upstream_ref', '_comment'])
|
|
|
89
93
|
*/
|
|
90
94
|
function normalizeText (text) {
|
|
91
95
|
if (typeof text !== 'string') return ''
|
|
92
|
-
|
|
96
|
+
// Horizontal whitespace collapses; newlines do not. A blank line is a
|
|
97
|
+
// paragraph break and a newline before a list marker is a list-item
|
|
98
|
+
// boundary, and both change what AsciiDoc renders -- the C++ source is one
|
|
99
|
+
// run-on sentence with no newlines at all, so treating \n the same as a
|
|
100
|
+
// plain space made an override that turns that wording into a real
|
|
101
|
+
// bulleted list or separate paragraphs compare byte-equal to source once
|
|
102
|
+
// "normalized". That classified it REDUNDANT, and the retirement step
|
|
103
|
+
// deletes an override wholesale, replacing a rendered list with a
|
|
104
|
+
// run-on paragraph carrying literal `*` characters. Verified against
|
|
105
|
+
// fetch_read_strategy in the live corpus: 2 rendered list items with the
|
|
106
|
+
// override, 0 (plus two stray asterisks) once "equal to source" deleted
|
|
107
|
+
// it.
|
|
108
|
+
return text
|
|
109
|
+
.split('\n')
|
|
110
|
+
.map((line) => line.replace(/[ \t]+/g, ' ').trim())
|
|
111
|
+
.join('\n')
|
|
112
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
113
|
+
.trim()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Separate a description's unconditional prose from its audience-scoped
|
|
118
|
+
* paragraphs.
|
|
119
|
+
*
|
|
120
|
+
* A description is either a plain string or an array of paragraphs, each
|
|
121
|
+
* optionally prefixed `cloud-only:` / `self-managed-only:`. Only the
|
|
122
|
+
* unconditional paragraphs are comparable to the source string: a paragraph
|
|
123
|
+
* written for one docs build is docs-only by definition and must never reach an
|
|
124
|
+
* upstream candidate, because the C++ doc string has no audience.
|
|
125
|
+
*
|
|
126
|
+
* The string form is returned unchanged, so every existing classification is
|
|
127
|
+
* byte-for-byte what it was before the array form existed.
|
|
128
|
+
*
|
|
129
|
+
* @param {string|Array<string>} description - Override description, either form.
|
|
130
|
+
* @returns {{prose: string, scoped: number, scopedParagraphs: string[]}} The
|
|
131
|
+
* unconditional prose, how many paragraphs were scoped away, and their text.
|
|
132
|
+
* The text is returned so a row can show a reviewer what it is holding back
|
|
133
|
+
* rather than dropping it silently.
|
|
134
|
+
*/
|
|
135
|
+
function unconditionalProse (description) {
|
|
136
|
+
if (typeof description === 'string') return { prose: description, scoped: 0, scopedParagraphs: [] }
|
|
137
|
+
if (!Array.isArray(description)) return { prose: '', scoped: 0, scopedParagraphs: [] }
|
|
138
|
+
const kept = []
|
|
139
|
+
const scopedParagraphs = []
|
|
140
|
+
for (const paragraph of description) {
|
|
141
|
+
if (typeof paragraph !== 'string' || !paragraph.trim()) continue
|
|
142
|
+
if (/^\s*(?:cloud-only|self-managed-only):/.test(paragraph)) {
|
|
143
|
+
scopedParagraphs.push(paragraph.trim())
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
kept.push(paragraph.trim())
|
|
147
|
+
}
|
|
148
|
+
return { prose: kept.join('\n\n'), scoped: scopedParagraphs.length, scopedParagraphs }
|
|
93
149
|
}
|
|
94
150
|
|
|
95
151
|
// Docs-only markup detectors. `<<anchor>>` cross-references are included
|
|
@@ -115,6 +171,12 @@ const MARKUP_PATTERNS = Object.freeze([
|
|
|
115
171
|
{ id: 'audience-prefix', re: /\b(?:self-managed-only|cloud-only):/ }
|
|
116
172
|
])
|
|
117
173
|
|
|
174
|
+
// Pulled out of MARKUP_PATTERNS by id, rather than re-typed, so the two
|
|
175
|
+
// checks that care about audience-scoping specifically -- the guard below,
|
|
176
|
+
// and MARKUP_PATTERNS' own detection -- cannot silently drift apart.
|
|
177
|
+
const AUDIENCE_PREFIX_RX = MARKUP_PATTERNS.find((p) => p.id === 'audience-prefix').re
|
|
178
|
+
const CONDITIONAL_RX = MARKUP_PATTERNS.find((p) => p.id === 'conditional').re
|
|
179
|
+
|
|
118
180
|
/**
|
|
119
181
|
* Find unescaped AsciiDoc attribute references (`{attr}`) that are not on
|
|
120
182
|
* the product-attr allowlist. Escaped literals (`\{attr}`) do not count.
|
|
@@ -264,7 +326,7 @@ function deepEqual (a, b) {
|
|
|
264
326
|
*/
|
|
265
327
|
function row (fields) {
|
|
266
328
|
const base = { name: fields.name, field: fields.field, class: fields.class }
|
|
267
|
-
for (const key of ['upstream_candidate_text', 'upstream_ref', 'content_hash', 'source_file', 'source_line', 'note']) {
|
|
329
|
+
for (const key of ['upstream_candidate_text', 'audience_scoped_text', 'upstream_ref', 'content_hash', 'source_file', 'source_line', 'note']) {
|
|
268
330
|
if (fields[key] !== undefined) base[key] = fields[key]
|
|
269
331
|
}
|
|
270
332
|
return base
|
|
@@ -281,7 +343,15 @@ function row (fields) {
|
|
|
281
343
|
*/
|
|
282
344
|
function classifyDescription (name, override, sourceProp, opts = {}) {
|
|
283
345
|
const attrAllowlist = opts.attrAllowlist || PRODUCT_ATTR_ALLOWLIST
|
|
284
|
-
|
|
346
|
+
// Audience-scoped paragraphs are filtered out here rather than inside
|
|
347
|
+
// stripDocsMarkup, which must keep stripping a prefix and retaining the
|
|
348
|
+
// content: for a see_also item the prefixed value IS the link, and dropping
|
|
349
|
+
// it there would silently empty every conditional related-topics entry.
|
|
350
|
+
const {
|
|
351
|
+
prose: overrideText,
|
|
352
|
+
scoped: scopedCount,
|
|
353
|
+
scopedParagraphs: scopedText
|
|
354
|
+
} = unconditionalProse(override.description)
|
|
285
355
|
const common = {
|
|
286
356
|
name,
|
|
287
357
|
field: 'description',
|
|
@@ -290,6 +360,13 @@ function classifyDescription (name, override, sourceProp, opts = {}) {
|
|
|
290
360
|
source_line: sourceProp && sourceProp.line_start !== undefined ? sourceProp.line_start : undefined
|
|
291
361
|
}
|
|
292
362
|
if (override.upstream_ref !== undefined) common.upstream_ref = override.upstream_ref
|
|
363
|
+
// Shown, never upstreamed. A scoped paragraph exists precisely so the other
|
|
364
|
+
// audience does not see it, and the C++ doc string is one string for every
|
|
365
|
+
// audience, so upstreaming it would both leak it to the wrong readers and
|
|
366
|
+
// duplicate it for the right ones (the override keeps its copy). Putting the
|
|
367
|
+
// text on the row lets a reviewer decide to upstream it and delete the
|
|
368
|
+
// paragraph in one deliberate change, instead of the audit deciding for them.
|
|
369
|
+
if (scopedText && scopedText.length > 0) common.audience_scoped_text = scopedText
|
|
293
370
|
|
|
294
371
|
if (!sourceProp) {
|
|
295
372
|
return row({
|
|
@@ -299,10 +376,43 @@ function classifyDescription (name, override, sourceProp, opts = {}) {
|
|
|
299
376
|
})
|
|
300
377
|
}
|
|
301
378
|
|
|
379
|
+
// A source property with no line_start has no description to replace. The
|
|
380
|
+
// topic extractor reports a name constant (topic_properties.h,
|
|
381
|
+
// handlers/topics/types.h) and fills description with an empty string, so a
|
|
382
|
+
// naive comparison reads "override differs from source" and proposes
|
|
383
|
+
// upstreaming prose into a file that has nowhere to put it. Measured against
|
|
384
|
+
// dev: 45 of 216 candidates, and not one of them in a .cc file, while every
|
|
385
|
+
// row the upstreaming workflow can actually edit carries a line.
|
|
386
|
+
//
|
|
387
|
+
// This is REVIEW rather than KEEP because the right answer is a judgement
|
|
388
|
+
// call: give the property a real doc string upstream, or accept that docs are
|
|
389
|
+
// the only place it is described. Never UPSTREAMABLE, which would hand the
|
|
390
|
+
// upstreaming workflow work it cannot do.
|
|
391
|
+
if (sourceProp.line_start === undefined) {
|
|
392
|
+
return row({
|
|
393
|
+
...common,
|
|
394
|
+
class: CLASSES.REVIEW,
|
|
395
|
+
note:
|
|
396
|
+
'Source defines this property\'s name but carries no description for it (' +
|
|
397
|
+
(sourceProp.defined_in || 'unknown file') +
|
|
398
|
+
'), so there is nothing to upstream into. Decide whether the source should gain one.'
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
302
402
|
const sourceText = normalizeText(sourceProp.description)
|
|
303
403
|
const overrideNormalized = normalizeText(overrideText)
|
|
304
404
|
|
|
305
405
|
if (overrideNormalized === sourceText) {
|
|
406
|
+
if (scopedCount > 0) {
|
|
407
|
+
// Retiring this would delete the audience-scoped paragraphs with it,
|
|
408
|
+
// because `description` replaces wholesale rather than merging. The
|
|
409
|
+
// redundant prose cannot be dropped on its own.
|
|
410
|
+
return row({
|
|
411
|
+
...common,
|
|
412
|
+
class: CLASSES.KEEP,
|
|
413
|
+
note: `Unconditional prose already matches source, but ${scopedCount} audience-scoped paragraph(s) are docs-only and would be lost with it. Nothing to upstream.`
|
|
414
|
+
})
|
|
415
|
+
}
|
|
306
416
|
return row({
|
|
307
417
|
...common,
|
|
308
418
|
class: CLASSES.REDUNDANT,
|
|
@@ -310,8 +420,55 @@ function classifyDescription (name, override, sourceProp, opts = {}) {
|
|
|
310
420
|
})
|
|
311
421
|
}
|
|
312
422
|
|
|
423
|
+
// A STRING description carrying a raw audience prefix or a hand-written
|
|
424
|
+
// ifdef/ifndef pair was never routed through the array form's
|
|
425
|
+
// paragraph-level scoping: unconditionalProse only strips a
|
|
426
|
+
// cloud-only:/self-managed-only: paragraph out of an ARRAY, and returns a
|
|
427
|
+
// string verbatim. Falling through to stripDocsMarkup below silently
|
|
428
|
+
// discards the audience information instead of refusing to guess at it: a
|
|
429
|
+
// raw "cloud-only:" prefix strips to a bare sentence with no trace it was
|
|
430
|
+
// ever Cloud-specific, and a hand-written ifdef/ifndef pair keeps BOTH
|
|
431
|
+
// branches' text concatenated as if they were one universal sentence.
|
|
432
|
+
// Either shape reaching upstream_candidate_text would write
|
|
433
|
+
// audience-specific or audience-merged prose into a C++ string every
|
|
434
|
+
// audience reads.
|
|
435
|
+
if (typeof override.description === 'string' &&
|
|
436
|
+
(AUDIENCE_PREFIX_RX.test(override.description) || CONDITIONAL_RX.test(override.description))) {
|
|
437
|
+
return row({
|
|
438
|
+
...common,
|
|
439
|
+
class: CLASSES.REVIEW,
|
|
440
|
+
note: 'This description is a plain string but contains an audience-scoped prefix or a hand-written conditional. Rewrite it as the array form (one entry per paragraph, prefixed cloud-only:/self-managed-only: where a paragraph is scoped) so the scoped text is never mistaken for universal prose.'
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// A description made ENTIRELY of audience-scoped paragraphs has no
|
|
445
|
+
// unconditional prose, so overrideText is the empty string. Without this
|
|
446
|
+
// guard it fell through to the SPLIT branch below and published an empty
|
|
447
|
+
// upstream_candidate_text -- which the upstream workflow selects, because
|
|
448
|
+
// the SPLIT note is part of its filter, and would then hand the model an
|
|
449
|
+
// empty string to write into an engineering doc string, blanking it.
|
|
450
|
+
if (overrideText.length === 0) {
|
|
451
|
+
return row({
|
|
452
|
+
...common,
|
|
453
|
+
class: CLASSES.REVIEW,
|
|
454
|
+
note: scopedCount > 0
|
|
455
|
+
? `Every paragraph of this description is audience-scoped (${scopedCount}), so there is no unconditional prose to upstream. Decide whether the scoped text belongs in source at all, or whether the property needs unconditional prose written for it.`
|
|
456
|
+
: 'The override sets an empty description. Decide whether it should be removed or given prose.'
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
313
460
|
const markupKinds = detectDocsMarkup(overrideText, attrAllowlist)
|
|
314
461
|
if (markupKinds.length === 0) {
|
|
462
|
+
if (scopedCount > 0) {
|
|
463
|
+
// The prose is upstreamable but the override still has to stay, to carry
|
|
464
|
+
// the scoped paragraphs -- which is exactly the SPLIT case.
|
|
465
|
+
return row({
|
|
466
|
+
...common,
|
|
467
|
+
class: CLASSES.KEEP_UNTIL_UPSTREAMED,
|
|
468
|
+
upstream_candidate_text: overrideText,
|
|
469
|
+
note: `SPLIT: unconditional prose differs from source and is markup-free, but ${scopedCount} audience-scoped paragraph(s) must stay in the override. Keep it until the prose ships.`
|
|
470
|
+
})
|
|
471
|
+
}
|
|
315
472
|
return row({
|
|
316
473
|
...common,
|
|
317
474
|
class: CLASSES.UPSTREAMABLE,
|
|
@@ -321,6 +478,20 @@ function classifyDescription (name, override, sourceProp, opts = {}) {
|
|
|
321
478
|
}
|
|
322
479
|
|
|
323
480
|
const stripped = stripDocsMarkup(overrideText)
|
|
481
|
+
// An include-only (or otherwise entirely-markup) description strips to
|
|
482
|
+
// nothing. Without this the SPLIT branch below would publish an empty
|
|
483
|
+
// upstream_candidate_text, which the upstream workflow selects (its
|
|
484
|
+
// filter matches on class and a SPLIT note, not on the text being
|
|
485
|
+
// non-empty) and would hand the model an empty string to write into a
|
|
486
|
+
// source description -- blanking it. The same failure the
|
|
487
|
+
// all-scoped-paragraphs guard above exists to prevent for the array form.
|
|
488
|
+
if (stripped.trim().length === 0) {
|
|
489
|
+
return row({
|
|
490
|
+
...common,
|
|
491
|
+
class: CLASSES.REVIEW,
|
|
492
|
+
note: `Stripping docs-only markup (${markupKinds.join(', ')}) leaves no prose at all, so there is nothing to upstream. Decide whether the source needs a real description written for it.`
|
|
493
|
+
})
|
|
494
|
+
}
|
|
324
495
|
if (normalizeText(stripped) === sourceText) {
|
|
325
496
|
return row({
|
|
326
497
|
...common,
|
|
@@ -530,6 +701,7 @@ function summarize (manifest) {
|
|
|
530
701
|
module.exports = {
|
|
531
702
|
CLASSES,
|
|
532
703
|
PRODUCT_ATTR_ALLOWLIST,
|
|
704
|
+
unconditionalProse,
|
|
533
705
|
KEEP_BY_DESIGN_FIELDS,
|
|
534
706
|
TYPO_KEYS,
|
|
535
707
|
normalizeText,
|
|
@@ -156,11 +156,13 @@ Create a `property-overrides.json` file to customize extracted documentation:
|
|
|
156
156
|
|
|
157
157
|
*Override fields:*
|
|
158
158
|
|
|
159
|
-
* `description` - Replace auto-extracted description
|
|
159
|
+
* `description` - Replace the auto-extracted description. Either a string, or an array of paragraphs when one of them belongs to a single docs build: an array entry may carry a `cloud-only:` / `self-managed-only:` prefix and the generator wraps it in the matching conditional. Use the array form rather than writing `ifdef::env-cloud[]` into the string. See <<Keeping a description upstreamable>>.
|
|
160
|
+
* `links` - Inline links, keyed by the literal text to find in the prose and valued by the target. See <<Adding links without forking the source prose>>.
|
|
161
|
+
* `includes` - Shared partials to pull into the property's entry, as Antora resource IDs. See <<Pulling in a shared partial>>.
|
|
160
162
|
* `example` - Add usage example in AsciiDoc format
|
|
161
163
|
* `example_file` - Load example from external file
|
|
162
164
|
* `version` - Document when property was introduced
|
|
163
|
-
* `see_also` - Array of related links. Each item is either a plain string (shown in every build) or an object `{content, cloud_only}` / `{content,
|
|
165
|
+
* `see_also` - Array of related links. Each item is either a plain string (shown in every build) or an object `{content, cloud_only}` / `{content, self_managed_only}` (shown only in that build, wrapped with `ifdef::env-cloud[]`/`ifndef::env-cloud[]` at generation time). Setting both `cloud_only` and `self_managed_only` on the same item is rejected by the schema — it would wrap the item in both directives at once, so it renders in neither build.
|
|
164
166
|
* `related_topics` - Deprecated predecessor to `see_also`. An array of plain strings, optionally prefixed `cloud-only:` / `self-managed-only:` for the same conditional behavior — that prefix is free text with no validation, which is why `see_also` replaced it. Existing `related_topics` entries keep working; new entries should use `see_also`.
|
|
165
167
|
|
|
166
168
|
Validate an overrides file against its schema (`docs-data/property-overrides.schema.json`) before generating docs:
|
|
@@ -170,6 +172,94 @@ Validate an overrides file against its schema (`docs-data/property-overrides.sch
|
|
|
170
172
|
npx doc-tools validate property-overrides --overrides docs-data/property-overrides.json
|
|
171
173
|
----
|
|
172
174
|
|
|
175
|
+
==== Adding links without forking the source prose
|
|
176
|
+
|
|
177
|
+
A property description is plain prose, and the links in it are declared separately:
|
|
178
|
+
|
|
179
|
+
[,json]
|
|
180
|
+
----
|
|
181
|
+
{
|
|
182
|
+
"properties": {
|
|
183
|
+
"advertised_rpc_api": {
|
|
184
|
+
"description": "Address of RPC endpoint published to other cluster members. If not set, the `rpc_server` broker property is used.",
|
|
185
|
+
"links": {
|
|
186
|
+
"`rpc_server`": "#rpc_server"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"recovery_mode_enabled": {
|
|
190
|
+
"description": "If `true`, start Redpanda in recovery mode, where user partitions are not loaded.",
|
|
191
|
+
"links": {
|
|
192
|
+
"recovery mode": "self-managed-only: xref:manage:recovery-mode.adoc"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
----
|
|
198
|
+
|
|
199
|
+
Each key is the literal text to find in the prose, backticks included, as written. Only the first occurrence is linked, so a property named four times links once. The longest key wins, so `+`cloud_storage_enabled`+` cannot be stolen by `+`cloud_storage`+`.
|
|
200
|
+
|
|
201
|
+
Three target forms:
|
|
202
|
+
|
|
203
|
+
`#<property_name>`:: Emits a `prop` macro. The macro resolves the reference page itself and renders plain code in a build that does not publish the target, so a property link needs no conditional and cannot dangle. A name that is not a known property is refused with a warning rather than emitted, because an unverifiable `prop` call publishes as literal macro text.
|
|
204
|
+
`xref:<target>`:: Emits a literal xref with the key as its link text. Property-page targets are canonicalized to `xref:reference:properties/<page>.adoc`.
|
|
205
|
+
`glossterm`:: Wraps the key in a glossary tooltip. The macro takes the term as its target and looks the definition up in the glossary, so the key is the term. Use `glossterm:<Term>` when the prose says something other than the term's own name.
|
|
206
|
+
|
|
207
|
+
A target may carry a `cloud-only:` / `self-managed-only:` prefix, in which case the containing paragraph is emitted twice: linked in that branch, plain in the other. AsciiDoc preprocessor directives have to start a line, so they cannot bracket an inline span; the paragraph is the smallest thing that can be wrapped. A scoped link that matches inside a delimited block is refused with a warning, since duplicating that paragraph would split the block across both branches.
|
|
208
|
+
|
|
209
|
+
The generator reports every key that matched no text. A link key that matches nothing is silent link rot: the override looks maintained and the page has no link.
|
|
210
|
+
|
|
211
|
+
Links are applied at generation time, so they reach the rendered partials but not the published `redpanda-properties-<tag>.json`. That file is the machine-readable data surface, and its descriptions drive hover tooltips, where an inline link is not wanted.
|
|
212
|
+
|
|
213
|
+
==== Pulling in a shared partial
|
|
214
|
+
|
|
215
|
+
Some entries end with a shared block: the "internal use only" warning, or the HTTP Proxy ephemeral-credentials breaking-change notice. Those are block-level docs structure, so they go in `includes` rather than into the description string:
|
|
216
|
+
|
|
217
|
+
[,json]
|
|
218
|
+
----
|
|
219
|
+
{
|
|
220
|
+
"properties": {
|
|
221
|
+
"data_transforms_read_buffer_memory_percentage": {
|
|
222
|
+
"description": "The percentage of available memory in the transform subsystem to use for read buffers.",
|
|
223
|
+
"includes": ["reference:partial$internal-use-property.adoc"]
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
----
|
|
228
|
+
|
|
229
|
+
Each entry is an Antora resource ID, rendered as an `include::` directive immediately after the description, which is where these were previously written by hand. An entry may carry a `cloud-only:` / `self-managed-only:` prefix, and its own `[attrs]` if it needs them; without brackets the generator adds the empty pair the directive requires. A value that does not look like a resource ID is emitted as given, with a warning, because an include with no family segment resolves to nothing at build time and does it silently.
|
|
230
|
+
|
|
231
|
+
==== Keeping a description upstreamable
|
|
232
|
+
|
|
233
|
+
`links`, `includes`, `see_also` and `admonitions` all exist so that a description can carry docs-site structure without mixing markup into the sentence. That is not a style preference. It is what makes the override retirable. `doc-tools overrides audit` compares an override's prose against the C++ doc string to decide whether it is redundant, upstreamable, or has to stay. Markup in the sentence makes the two incomparable, so an enriched description could never be retired or sent upstream. A hand-written `ifdef::env-cloud[]` does the same thing, which is why the array form exists.
|
|
234
|
+
|
|
235
|
+
An audience-scoped paragraph is docs-only by definition, because the source string has no audience, so the audit compares only the unconditional paragraphs and never puts scoped text into an upstream candidate.
|
|
236
|
+
|
|
237
|
+
==== Conditional admonitions
|
|
238
|
+
|
|
239
|
+
An `admonitions` entry takes the same `cloud_only` / `self_managed_only` booleans as a `see_also` object, wrapping the whole block:
|
|
240
|
+
|
|
241
|
+
[,json]
|
|
242
|
+
----
|
|
243
|
+
{
|
|
244
|
+
"admonitions": [
|
|
245
|
+
{ "type": "note", "text": "Applies to every build." },
|
|
246
|
+
{ "type": "tip", "text": "Cloud readers only.", "cloud_only": true }
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
----
|
|
250
|
+
|
|
251
|
+
==== One name per concept
|
|
252
|
+
|
|
253
|
+
The audience flags are `cloud-only:` / `self-managed-only:` as a string prefix, and `cloud_only` / `self_managed_only` as object booleans. The hyphens and underscores differ because one is prose and the other is a JSON key in a snake_case file, but the *word* is the same in both: self-managed, matching the product name.
|
|
254
|
+
|
|
255
|
+
`self_hosted_only` was the old spelling of that boolean, so the same concept went by two different words depending on how you wrote it. It still parses, and normalizes to `self_managed_only`, so nothing emits it. `docs-data/rpk-overrides.json` still spells its equivalent `selfHostedOnly` (camelCase, to match that file's own key style) and carries 34 live entries, so it needs its own rename.
|
|
256
|
+
|
|
257
|
+
==== Conditionals and blank lines
|
|
258
|
+
|
|
259
|
+
Every conditional the generator emits has a blank line before it and after its `endif::[]`. This is load-bearing. A conditional is resolved by the preprocessor, so one glued to neighbouring prose leaves those lines adjacent and Asciidoctor merges them into a single paragraph. Worse, when the conditional wraps a delimited block it merges them in only one branch, so a self-managed preview can look correct while Cloud loses its paragraph breaks. Nothing errors and no text goes missing, so only a structural assertion on rendered HTML catches it.
|
|
260
|
+
|
|
261
|
+
The generator warns about any conditional it finds glued, including one an override still writes by hand. `__tests__/tools/property-extractor/conditional-rendering.test.js` pins the measured behaviour and proves both detectors fire; `property-corpus-rendering.test.js` runs the whole live overrides corpus through Asciidoctor in both builds.
|
|
262
|
+
|
|
173
263
|
This catches malformed `see_also` items and unrecognized override keys — including a typo that would otherwise be silently dropped, since applying an override to an *existing* property has no catch-all pass-through (only overrides that create a new property from scratch do).
|
|
174
264
|
|
|
175
265
|
=== Understanding property changes
|
|
@@ -1130,6 +1220,55 @@ npx jest __tests__/tools/topic_property_extractor.test.js
|
|
|
1130
1220
|
npm test -- --coverage
|
|
1131
1221
|
----
|
|
1132
1222
|
|
|
1223
|
+
==== The live overrides corpus
|
|
1224
|
+
|
|
1225
|
+
`__tests__/tools/property-extractor/property-corpus-rendering.test.js` runs the real
|
|
1226
|
+
overrides file, not a fixture, because a fixture passes while the live 438-entry corpus
|
|
1227
|
+
breaks. Two files feed it, both mirrors of the `docs` repo:
|
|
1228
|
+
|
|
1229
|
+
`__tests__/docs-data/property-overrides.json`:: A byte copy of `docs-data/property-overrides.json`.
|
|
1230
|
+
`__tests__/docs-data/property-snapshot.json`:: Every extracted property, reduced to the
|
|
1231
|
+
fields the rendering and link layers read. `cloud_supported` is the point: it is what
|
|
1232
|
+
makes the "no Cloud link to a property Cloud does not publish" assertion real rather
|
|
1233
|
+
than assumed. Descriptions are deliberately absent, because the corpus test drives prose from
|
|
1234
|
+
the overrides file, and a full copy would be 695 KB of text that rots.
|
|
1235
|
+
|
|
1236
|
+
Keep the overrides copy a faithful mirror. It previously doubled as
|
|
1237
|
+
`property-docs-overrides.test.js`'s fixture, someone hand-edited an `admin` entry into
|
|
1238
|
+
it that never existed in the `docs` repo, and refreshing it would then have broken that
|
|
1239
|
+
test, so it sat three months and 114 entries behind. That test now has its own
|
|
1240
|
+
`property-overrides-admin-fixture.json`; put new fixture data there or in the test
|
|
1241
|
+
itself, never in the mirror.
|
|
1242
|
+
|
|
1243
|
+
Refresh both from a `docs` checkout:
|
|
1244
|
+
|
|
1245
|
+
[,bash]
|
|
1246
|
+
----
|
|
1247
|
+
DOCS=../docs # path to your docs repo checkout
|
|
1248
|
+
|
|
1249
|
+
git -C "$DOCS" show origin/main:docs-data/property-overrides.json \
|
|
1250
|
+
> __tests__/docs-data/property-overrides.json
|
|
1251
|
+
|
|
1252
|
+
git -C "$DOCS" show origin/main:modules/reference/attachments/redpanda-properties-v26.2.2.json \
|
|
1253
|
+
| node -e '
|
|
1254
|
+
const fs = require("fs");
|
|
1255
|
+
const src = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
1256
|
+
const keep = ["name","config_scope","type","cloud_supported","cloud_editable",
|
|
1257
|
+
"cloud_readonly","cloud_byoc_only","is_deprecated","nullable"];
|
|
1258
|
+
const properties = {};
|
|
1259
|
+
for (const [k, v] of Object.entries(src.properties || {})) {
|
|
1260
|
+
properties[k] = Object.fromEntries(keep.filter((f) => f in v).map((f) => [f, v[f]]));
|
|
1261
|
+
}
|
|
1262
|
+
const out = JSON.parse(fs.readFileSync("__tests__/docs-data/property-snapshot.json", "utf8"));
|
|
1263
|
+
out.properties = properties;
|
|
1264
|
+
fs.writeFileSync("__tests__/docs-data/property-snapshot.json", JSON.stringify(out, null, 1));
|
|
1265
|
+
'
|
|
1266
|
+
----
|
|
1267
|
+
|
|
1268
|
+
The `docs` repo fails its own CI when the overrides mirror drifts, following the
|
|
1269
|
+
`sync-schemas --check` pattern; this repo's CI has no `docs` checkout, so the check
|
|
1270
|
+
cannot live here.
|
|
1271
|
+
|
|
1133
1272
|
=== Making changes
|
|
1134
1273
|
|
|
1135
1274
|
1. **Before you start:**
|
|
@@ -225,8 +225,18 @@ function compareProperties(oldData, newData, oldVersion, newVersion) {
|
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// Check for description changes
|
|
229
|
-
|
|
228
|
+
// Check for description changes. deepEqual, not !==: an array-form
|
|
229
|
+
// description (audience-scoped paragraphs) is a fresh array on both
|
|
230
|
+
// sides of every comparison, since each side comes from parsing its
|
|
231
|
+
// own attachment JSON, so reference inequality reported EVERY
|
|
232
|
+
// property carrying one as changed, forever, with identical old and
|
|
233
|
+
// new text -- permanent noise in the one section of the release PR a
|
|
234
|
+
// reviewer reads to spot real engineering wording changes. In
|
|
235
|
+
// practice this attachment is flattened to a string before it ships
|
|
236
|
+
// (see generate-handlebars-docs.js), so deepEqual and !== agree on a
|
|
237
|
+
// real release comparison; this guards the case where compare runs
|
|
238
|
+
// against a pre-flatten JSON, or the flatten step is ever skipped.
|
|
239
|
+
if (!deepEqual(oldProp.description, newProp.description)) {
|
|
230
240
|
report.changedDescriptions.push({
|
|
231
241
|
name,
|
|
232
242
|
oldDescription: oldProp.description || 'No description',
|