@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.56.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 (87) hide show
  1. package/bin/doc-tools-mcp.js +4 -5
  2. package/bin/doc-tools.js +354 -9
  3. package/bin/mcp-tools/generated-docs-review.js +26 -9
  4. package/bin/mcp-tools/overrides-audit.js +1 -1
  5. package/bin/mcp-tools/rpcn-docs.js +4 -14
  6. package/cli-utils/docs-links-to-xrefs.js +174 -0
  7. package/cli-utils/format-helm-spec.js +4 -3
  8. package/cli-utils/generated-marker.js +43 -0
  9. package/cli-utils/helm-commented-values.js +125 -21
  10. package/cli-utils/self-managed-docs-branch.js +36 -30
  11. package/extension-utils/html-utils.js +27 -0
  12. package/extension-utils/llms-utils.js +6 -2
  13. package/extension-utils/metadata-candidates.js +304 -0
  14. package/extension-utils/metadata-check.js +174 -0
  15. package/extension-utils/metadata-suggest.js +573 -0
  16. package/extension-utils/taxonomy-categories.js +106 -0
  17. package/extension-utils/taxonomy.js +566 -0
  18. package/extensions/REFERENCE.adoc +110 -8
  19. package/extensions/add-git-dates.js +13 -1
  20. package/extensions/aggregate-terms.js +34 -20
  21. package/extensions/algolia-indexer/generate-index.js +13 -1
  22. package/extensions/algolia-indexer/index.js +4 -2
  23. package/extensions/convert-sitemap-to-markdown.js +47 -12
  24. package/extensions/convert-to-markdown.js +176 -0
  25. package/extensions/generate-fields-only-pages.js +14 -0
  26. package/extensions/modify-connect-tag-playbook.js +181 -3
  27. package/extensions/resolve-xrefs-in-attachments.js +1 -12
  28. package/extensions/url-to-xref.js +92 -22
  29. package/extensions/util/connect-docs-asset.js +6 -5
  30. package/extensions/validate-attributes.js +10 -1
  31. package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
  32. package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
  33. package/extensions/version-fetcher/list-connect-releases.js +3 -3
  34. package/extensions/version-fetcher/release-lines.js +64 -0
  35. package/extensions/version-fetcher/set-latest-version.js +73 -3
  36. package/macros/README.adoc +1 -0
  37. package/macros/REFERENCE.adoc +99 -0
  38. package/macros/badge.js +5 -8
  39. package/macros/enterprise.js +2 -5
  40. package/macros/glossary.js +4 -14
  41. package/macros/iceberg-explorer.js +242 -0
  42. package/macros/prop.js +3 -13
  43. package/mcp/USER_GUIDE.adoc +25 -135
  44. package/package.json +5 -2
  45. package/tools/check-metadata/index.js +523 -0
  46. package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
  47. package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
  48. package/tools/cloud-regions/render-cloud-regions.js +3 -0
  49. package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
  50. package/tools/dispatch-graph/classify-runs.js +128 -0
  51. package/tools/dispatch-graph/parse-workflows.js +499 -0
  52. package/tools/doc-impact-recording/index.js +190 -0
  53. package/tools/enterprise-features/detect-gating-changes.js +393 -0
  54. package/tools/enterprise-features/verify.js +2 -1
  55. package/tools/gen-rpk-ascii.py +7 -1
  56. package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
  57. package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
  58. package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
  59. package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
  60. package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
  61. package/tools/lint-strings/index.js +133 -3
  62. package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
  63. package/tools/lint-strings/surfaces/connect.js +72 -12
  64. package/tools/lint-strings/surfaces/crd.js +10 -6
  65. package/tools/metrics/metrics.py +11 -0
  66. package/tools/overrides-audit/README.adoc +1 -2
  67. package/tools/overrides-audit/adapters/properties.js +1 -1
  68. package/tools/overrides-audit/index.js +3 -4
  69. package/tools/property-extractor/generate-handlebars-docs.js +9 -1
  70. package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
  71. package/tools/redpanda-connect/AUTOMATION.md +3 -36
  72. package/tools/redpanda-connect/README.adoc +5 -33
  73. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +32 -11
  74. package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
  75. package/tools/redpanda-connect/report-delta.js +6 -3
  76. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +42 -25
  77. package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
  78. package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
  79. package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
  80. package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
  81. package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
  82. package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
  83. package/tools/rpk-docs/generate-rpk-docs.js +2 -0
  84. package/tools/rpk-docs/generate-x-env-partial.js +7 -4
  85. package/tools/rpk-docs/helpers/index.js +4 -1
  86. package/tools/rpk-docs/templates/command.hbs +2 -2
  87. package/tools/overrides-audit/adapters/connect.js +0 -144
@@ -10,7 +10,6 @@
10
10
  * - Context-aware: Works from any repository based on cwd
11
11
  * - Antora intelligence: Understands component/module structure
12
12
  * - Automation: Run doc-tools generate commands
13
- * - Auto-discovery: Prompts loaded automatically from mcp/prompts/
14
13
  * - Validation: Startup checks ensure all resources are accessible
15
14
  * - Telemetry: Usage tracking for adoption metrics
16
15
  */
@@ -201,7 +200,7 @@ const tools = [
201
200
  },
202
201
  {
203
202
  name: 'generate_rpcn_connector_docs',
204
- description: 'Generate Redpanda Connect connector documentation. Creates component documentation for all connectors. Writers use this when updating connector reference docs. Automatically uses docs-data/overrides.json if present.',
203
+ description: 'Generate Redpanda Connect connector documentation. Creates component documentation for all connectors. Writers use this when updating connector reference docs. Field, component, and Bloblang descriptions come from the component specs in redpanda-data/connect and redpanda-data/benthos, so fix wrong text with a PR there.',
205
204
  inputSchema: {
206
205
  type: 'object',
207
206
  properties: {
@@ -239,7 +238,7 @@ const tools = [
239
238
  },
240
239
  overrides: {
241
240
  type: 'string',
242
- description: 'Path to optional JSON file with overrides (defaults to docs-data/overrides.json if present)'
241
+ description: 'Path to an optional JSON file of description overrides (optional). Nothing is merged unless you pass it.'
243
242
  },
244
243
  cloud_version: {
245
244
  type: 'string',
@@ -516,7 +515,7 @@ const tools = [
516
515
  surface: {
517
516
  type: 'string',
518
517
  description: 'Override surface to audit (optional, defaults to "properties")',
519
- enum: ['properties', 'rpk', 'connect']
518
+ enum: ['properties', 'rpk']
520
519
  },
521
520
  output: {
522
521
  type: 'string',
@@ -594,7 +593,7 @@ const resourceMap = {
594
593
 
595
594
  /**
596
595
  * Load resource content from team-standards directory
597
- * @param {string} uri - Resource URI (such as 'redpanda://style-guide')
596
+ * @param {string} uri - Resource URI (such as 'redpanda://personas')
598
597
  * @returns {Object} Resource content
599
598
  */
600
599
  function getResourceContent(uri) {
package/bin/doc-tools.js CHANGED
@@ -43,6 +43,7 @@ const { getAntoraValue, setAntoraValue } = require('../cli-utils/antora-utils')
43
43
  const programCli = new Command()
44
44
 
45
45
  const pkg = require('../package.json')
46
+ const { generatedMarker } = require('../cli-utils/generated-marker')
46
47
  programCli
47
48
  .name('doc-tools')
48
49
  .description('Redpanda Document Automation CLI')
@@ -755,6 +756,111 @@ programCli
755
756
  console.log(' mcp/README.adoc')
756
757
  })
757
758
 
759
+ /**
760
+ * check-dispatch-graph
761
+ *
762
+ * @description
763
+ * Health check for the repository_dispatch graph that connects Redpanda's
764
+ * engineering repos to its docs repos. Reads every workflow file on each
765
+ * configured repo's default branch, pairs the senders (peter-evans/repository-dispatch,
766
+ * `gh api .../dispatches`, curl, github-script) with the listeners
767
+ * (`on.repository_dispatch.types`), and reads each workflow's recent runs from
768
+ * the Actions API. Reports senders with no listener, docs listeners with no
769
+ * sender (informational when a cron backstops them), senders that have never
770
+ * fired, receivers failing more than the threshold, receiver payload variants
771
+ * no sender dispatches, and edges dispatched from more than one repo.
772
+ *
773
+ * @why
774
+ * A repository_dispatch is accepted with HTTP 204 whether or not a listener
775
+ * exists, and a receiver that fails every run looks, from the sender's side,
776
+ * exactly like one that works. Breaks in this graph went unnoticed for weeks
777
+ * until someone read the receivers' run history by hand. This command reads it
778
+ * by script, and .github/workflows/check-dispatch-graph.yml runs it weekly,
779
+ * files one issue, and closes it on the first run that finds the graph healthy.
780
+ *
781
+ * Exit codes: 0 clean, 1 findings (the output ends with the line
782
+ * DISPATCH_GRAPH_FINDINGS_CONFIRMED), 2 could not run (a repo or runs query
783
+ * could not be read, so the graph is incomplete; never reported as clean).
784
+ *
785
+ * @example
786
+ * # Check the default repo set against the live org
787
+ * export REDPANDA_GITHUB_TOKEN=... # or GH_TOKEN; must read the private docs repos
788
+ * npx doc-tools check-dispatch-graph
789
+ *
790
+ * # Machine-readable
791
+ * npx doc-tools check-dispatch-graph --json > dispatch-graph.json
792
+ *
793
+ * # A narrower set of repos (role decides which side a missing counterpart is reported on)
794
+ * npx doc-tools check-dispatch-graph --repos redpanda-data/helm-charts:engineering,redpanda-data/docs:docs
795
+ *
796
+ * @requirements
797
+ * - A GitHub token that can read every configured repo and its Actions runs
798
+ * (REDPANDA_GITHUB_TOKEN, ACTIONS_BOT_TOKEN, GITHUB_TOKEN or GH_TOKEN). The
799
+ * docs repos are private, so an unauthenticated run exits 2.
800
+ */
801
+ programCli
802
+ .command('check-dispatch-graph')
803
+ .description('Check the repository_dispatch graph between engineering and docs repos: senders without listeners, listeners without senders, never-fired senders, failing receivers (exit 0 clean, 1 findings, 2 could not run)')
804
+ .option('--repos <list>', 'Comma-separated owner/name[:role] entries (role engineering or docs) replacing the default repo set')
805
+ .option('--fail-threshold <fraction>', 'Flag a receiver whose fail rate over the window is above this fraction', '0.5')
806
+ .option('--window <n>', 'How many of the newest runs the fail rate is computed over', '20')
807
+ .option('--per-page <n>', 'How many runs to fetch per workflow (max 100)', '50')
808
+ .option('--json', 'Print the full result as JSON instead of the table')
809
+ .action(async (options) => {
810
+ const { runCheck, formatReport, createGitHubApi, DEFAULT_REPOS, SENTINEL } = require('../tools/dispatch-graph/check-dispatch-graph')
811
+
812
+ let repos = DEFAULT_REPOS
813
+ if (options.repos) {
814
+ repos = String(options.repos).split(',').map((entry) => entry.trim()).filter(Boolean).map((entry) => {
815
+ const [repo, role = 'docs'] = entry.split(':')
816
+ if (!/^[^/]+\/[^/]+$/.test(repo) || !['engineering', 'docs'].includes(role)) {
817
+ console.error(`Error: --repos entry "${entry}" must be owner/name or owner/name:engineering|docs`)
818
+ process.exit(2)
819
+ }
820
+ return { repo, role }
821
+ })
822
+ }
823
+ const threshold = Number(options.failThreshold)
824
+ const window = Number(options.window)
825
+ const perPage = Number(options.perPage)
826
+ if (!(threshold >= 0 && threshold <= 1) || !(window > 0) || !(perPage > 0 && perPage <= 100)) {
827
+ console.error('Error: --fail-threshold must be 0..1, --window > 0, --per-page 1..100')
828
+ process.exit(2)
829
+ }
830
+
831
+ // Exit 2 is reserved for "could not find out", so the weekly job can tell
832
+ // "an edge is broken" apart from "the API was down" and not file a false
833
+ // issue. An unauthenticated run cannot read the private docs repos and
834
+ // lands here too; it must never pass as clean.
835
+ let result
836
+ try {
837
+ const octokit = require('../cli-utils/octokit-client')
838
+ result = await runCheck({ repos, api: createGitHubApi(octokit), threshold, window, perPage })
839
+ } catch (err) {
840
+ console.error(`Error: Could not check the dispatch graph: ${err.message}`)
841
+ process.exit(2)
842
+ }
843
+
844
+ if (options.json) {
845
+ console.log(JSON.stringify(result, null, 2))
846
+ } else {
847
+ console.log(formatReport(result))
848
+ }
849
+
850
+ if (result.exitCode === 2) {
851
+ console.error(`\nError: ${result.unreadable.length} repo(s) or run record(s) could not be read, so the graph is incomplete. Not a clean result.`)
852
+ process.exit(2)
853
+ }
854
+ if (result.exitCode === 1) {
855
+ // Printed only where findings were actually established; the weekly
856
+ // script requires this line before filing an issue, so a usage error or
857
+ // a crash (both exit 1 under Commander) can never file one.
858
+ if (!options.json) console.log(`\n${SENTINEL}`)
859
+ process.exit(1)
860
+ }
861
+ process.exit(0)
862
+ })
863
+
758
864
  // ====================================================================
759
865
  // GENERATE SUBCOMMAND GROUP
760
866
  // ====================================================================
@@ -896,7 +1002,7 @@ automation
896
1002
  .option('--template-metadata <path>', 'Metadata section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/metadata-partials.hbs'))
897
1003
  .option('--template-description <path>', 'Description section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/descriptions-partials.hbs'))
898
1004
  .option('--template-bloblang <path>', 'Custom Handlebars template for bloblang function/method partials')
899
- .option('--overrides <path>', 'Optional JSON file with overrides', 'docs-data/overrides.json')
1005
+ .option('--overrides <path>', 'Optional JSON file of description overrides to merge into the generated output. Nothing is merged unless you pass it. Connector descriptions come from the connect and benthos component specs, so fix wrong text there')
900
1006
  .option('--include-bloblang', 'Include Bloblang functions and methods in generation')
901
1007
  .option('--no-partials', 'Skip the generated partials, config snippets, and Bloblang reference because the connect repo publishes them. What\'s new, diffs, drafts, nav, and the version bump still run')
902
1008
  .option('--prune-orphaned-descriptions', 'Allow the description-partial orphan sweep to blank more than 10% of the tree. Only use this when the connector dataset is confirmed complete: an incomplete dataset looks identical to a mass upstream deletion and the sweep blanks published content')
@@ -1824,17 +1930,25 @@ automation
1824
1930
  const outFile = path.join(outDir, `k-${name}${opts.outputSuffix}`)
1825
1931
  console.log(`pandoc ${md} → ${outFile}`)
1826
1932
  fs.mkdirSync(path.dirname(outFile), { recursive: true })
1827
- r = spawnSync('pandoc', [md, '-t', 'asciidoc', '-o', outFile], { stdio: 'inherit' })
1828
- if (r.status !== 0) process.exit(r.status)
1933
+ const { markdownToAsciiDoc } = require('../cli-utils/helm-commented-values')
1934
+ let converted
1935
+ try {
1936
+ converted = markdownToAsciiDoc(fs.readFileSync(md, 'utf8')) + '\n'
1937
+ } catch (err) {
1938
+ console.error(`Error: ${err.message}`)
1939
+ process.exit(1)
1940
+ }
1829
1941
 
1830
1942
  const { formatHelmSpec } = require('../cli-utils/format-helm-spec')
1831
- let doc = formatHelmSpec(fs.readFileSync(outFile, 'utf8'))
1943
+ let doc = formatHelmSpec(converted)
1832
1944
 
1833
1945
  // helm-docs only renders keys that exist in the YAML tree, so optional
1834
1946
  // values that ship commented out never appear in the reference. Pull
1835
1947
  // documented commented-out keys from values.yaml and inject them. This
1836
1948
  // runs after formatHelmSpec so the injected body sections are not affected
1837
- // by the blank-line normalization that its header fix performs.
1949
+ // by the blank-line normalization that its header fix performs. The
1950
+ // comments are Markdown, so each one goes through markdownToAsciiDoc on
1951
+ // the way in, like the README did.
1838
1952
  const valuesFile = path.join(chartPath, 'values.yaml')
1839
1953
  if (fs.existsSync(valuesFile)) {
1840
1954
  const { extractCommentedValueDocs, injectIntoAsciiDoc, filterEntriesBySchema } = require('../cli-utils/helm-commented-values')
@@ -1856,7 +1970,7 @@ automation
1856
1970
  }
1857
1971
 
1858
1972
  if (entries.length > 0) {
1859
- const { doc: withInjected, injected, sectionsFound } = injectIntoAsciiDoc(doc, entries)
1973
+ const { doc: withInjected, injected, sectionsFound } = injectIntoAsciiDoc(doc, entries, { toAsciiDoc: markdownToAsciiDoc })
1860
1974
  doc = withInjected
1861
1975
  if (injected.length > 0) {
1862
1976
  console.log(`Documented ${injected.length} commented-out value(s): ${injected.join(', ')}`)
@@ -1871,6 +1985,10 @@ automation
1871
1985
  }
1872
1986
  }
1873
1987
 
1988
+ // Same marker shape as every doc-tools generator; convert-to-markdown
1989
+ // reads it and reports the provenance in the Markdown twins.
1990
+ const helmMarker = generatedMarker('helm-docs', `the ${name} chart's values.yaml and README in redpanda-data/redpanda-operator`) + '\n\n'
1991
+ doc = helmMarker + doc
1874
1992
  fs.writeFileSync(outFile, doc, 'utf8')
1875
1993
 
1876
1994
  console.log(`Done: Wrote ${outFile}`)
@@ -2134,6 +2252,12 @@ automation
2134
2252
  * - Use --tag for released content (GA or beta releases). Tags reference specific release points.
2135
2253
  * - Use --branch for in-progress content (unreleased features). Branches track ongoing development.
2136
2254
  *
2255
+ * Links: the Go doc comments link to docs.redpanda.com with absolute URLs. When the command
2256
+ * runs inside a docs checkout (or is given --docs-root), every URL that matches a page of that
2257
+ * checkout, directly or through a page alias, is rewritten as an xref so the committed page
2258
+ * keeps one link form across regenerations. URLs that match no page are left as written and
2259
+ * listed in the output.
2260
+ *
2137
2261
  * @why
2138
2262
  * Kubernetes CRDs define complex APIs for deploying and managing Redpanda. The API schema
2139
2263
  * is defined in Go code with hundreds of fields across nested structures. Manual documentation
@@ -2168,6 +2292,7 @@ automation
2168
2292
  * - For GitHub URLs: Git and internet connection
2169
2293
  * - crd-ref-docs tool (automatically installed if missing)
2170
2294
  * - Go toolchain for running crd-ref-docs
2295
+ * - To convert links: a docs checkout with Antora installed (npm install in redpanda-data/docs)
2171
2296
  */
2172
2297
  automation
2173
2298
  .command('crd-spec')
@@ -2178,6 +2303,7 @@ automation
2178
2303
  .option('-d, --depth <n>', 'How many levels deep', '10')
2179
2304
  .option('--templates-dir <dir>', 'Asciidoctor templates dir', '.github/crd-config/templates/asciidoctor/operator')
2180
2305
  .option('--output <file>', 'Where to write the generated AsciiDoc file', 'modules/reference/pages/k-crd.adoc')
2306
+ .option('--docs-root <dir>', 'Docs checkout whose pages docs.redpanda.com URLs are converted into xrefs against (default: the current repo when run inside redpanda-data/docs)')
2181
2307
  .action(async (opts) => {
2182
2308
  verifyCrdDependencies()
2183
2309
 
@@ -2286,8 +2412,28 @@ automation
2286
2412
  process.exit(1)
2287
2413
  }
2288
2414
 
2289
- // docs.redpanda.com URLs are left as-is: the url-to-xref Antora
2290
- // extension converts them to validated xrefs at site build time.
2415
+ // Go doc comments carry absolute docs.redpanda.com URLs (a Go comment
2416
+ // cannot hold an xref). Convert them against the docs checkout so the
2417
+ // committed page keeps one link form across regenerations, instead of
2418
+ // flipping between xrefs and URLs and burying the real changes in the
2419
+ // review diff (docs 2256). A URL that matches no page in the checkout is
2420
+ // left as written and listed; the url-to-xref extension still converts
2421
+ // cross-component URLs at site build time and warns about broken ones.
2422
+ const docsRoot = opts.docsRoot || (inDocs ? repoRoot : null)
2423
+ if (docsRoot) {
2424
+ const { buildDocsResolverContext, convertDocsLinksInFile, formatDocsLinkReport } = require('../cli-utils/docs-links-to-xrefs.js')
2425
+ let linkContext
2426
+ try {
2427
+ linkContext = await buildDocsResolverContext(docsRoot)
2428
+ } catch (err) {
2429
+ console.error(`Error: Cannot map docs URLs to xrefs: ${err.message}`)
2430
+ process.exit(1)
2431
+ }
2432
+ console.log(`Mapping docs URLs against ${linkContext.pageCount} pages of the ${linkContext.component} component in ${docsRoot}`)
2433
+ console.log(formatDocsLinkReport(convertDocsLinksInFile(opts.output, linkContext), opts.output))
2434
+ } else {
2435
+ console.warn('⚠️ No docs checkout to map against (pass --docs-root); docs.redpanda.com URLs are left as written.')
2436
+ }
2291
2437
 
2292
2438
  if (tmpSrc) fs.rmSync(tmpSrc, { recursive: true, force: true })
2293
2439
  fs.rmSync(configTmp, { recursive: true, force: true })
@@ -2875,12 +3021,104 @@ programCli
2875
3021
  .option('--skip-rules <list>', 'Comma-separated rule ids to skip')
2876
3022
  .option('--only-rules <list>', 'Comma-separated rule ids to run exclusively')
2877
3023
  .option('--reviewed <file>', 'Diff mode: skip declarations whose fingerprint is listed in <file> (already reviewed on an earlier push)')
3024
+ .option('--reviewer-comments <file>', 'Diff mode: skip declarations whose text a human reviewer wrote in one of the comment bodies in <file> (a JSON array of strings)')
2878
3025
  .option('--strict', 'Exit 1 when any error-severity finding exists (default: always exit 0 - suggest, never block)')
2879
3026
  .action((options) => {
2880
3027
  const { runCli } = require('../tools/lint-strings')
2881
3028
  runCli(options)
2882
3029
  })
2883
3030
 
3031
+ /**
3032
+ * detect-enterprise-gating
3033
+ *
3034
+ * @description
3035
+ * Find enterprise gating changes in a Redpanda core diff that no doc string
3036
+ * announces: a value added to or removed from license_required_feature in
3037
+ * src/v/features/enterprise_features.h, a property wrapped in or unwrapped
3038
+ * from config::enterprise<> in src/v/config/configuration.h, or a changed
3039
+ * restricted value of an enterprise<> property in
3040
+ * src/v/config/configuration.cc. Prints them as a
3041
+ * doc-impact report, the JSON the doc-strings review dispatches to the docs
3042
+ * team's Jira router, so a licensing change reaches a DOC ticket even when
3043
+ * the review has no doc string to look at.
3044
+ *
3045
+ * @example
3046
+ * npx doc-tools detect-enterprise-gating --repo . --diff origin/dev
3047
+ *
3048
+ * @exit 0 always: `{"findings":[]}` means nothing to report. Errors from git
3049
+ * print to stderr and still exit 0 with an empty report, so a caller's
3050
+ * fail-open gate stays closed rather than red.
3051
+ */
3052
+ programCli
3053
+ .command('detect-enterprise-gating')
3054
+ .description('Find enterprise gating changes (license_required_feature values, config::enterprise<> wrappers and restricted values) in a core diff and print them as a doc-impact report')
3055
+ .requiredOption('--repo <path>', 'Path to a Redpanda core checkout. Nothing is cloned.')
3056
+ .requiredOption('--diff <base>', 'Base ref: changes in <base>...HEAD are inspected')
3057
+ .option('--format <format>', 'Output format: json or human', 'json')
3058
+ .action((options) => {
3059
+ const { spawnSync } = require('child_process')
3060
+ const { detectGatingChanges, buildDocImpact, enterprisePropertyNames, ENUM_HEADER, CONFIG_HEADER, CONFIG_SOURCE } = require('../tools/enterprise-features/detect-gating-changes')
3061
+ const empty = { findings: [] }
3062
+ const result = spawnSync('git', ['diff', '--no-color', '--no-ext-diff', `${options.diff}...HEAD`, '--', ENUM_HEADER, CONFIG_HEADER, CONFIG_SOURCE], {
3063
+ cwd: options.repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024
3064
+ })
3065
+ if (result.status !== 0) {
3066
+ console.error(`git diff ${options.diff}...HEAD failed in ${options.repo}: ${(result.stderr || '').trim()}`)
3067
+ console.log(JSON.stringify(empty))
3068
+ return
3069
+ }
3070
+ // A configuration.cc change is tied to an enterprise property through
3071
+ // the header at HEAD. Without it, header and enum changes still report.
3072
+ const header = spawnSync('git', ['show', `HEAD:${CONFIG_HEADER}`], { cwd: options.repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
3073
+ if (header.status !== 0) console.error(`git show HEAD:${CONFIG_HEADER} failed in ${options.repo}; configuration.cc changes are not checked: ${(header.stderr || '').trim()}`)
3074
+ const enterpriseNames = header.status === 0 ? enterprisePropertyNames(header.stdout) : []
3075
+ const findings = detectGatingChanges(result.stdout, { enterpriseNames })
3076
+ const report = buildDocImpact(findings) || empty
3077
+ if (options.format === 'human') {
3078
+ if (findings.length === 0) { console.log('No enterprise gating changes.'); return }
3079
+ for (const f of findings) console.log(`${f.change_kind}: ${f.name}`)
3080
+ return
3081
+ }
3082
+ console.log(JSON.stringify(report, null, 2))
3083
+ })
3084
+
3085
+ /**
3086
+ * doc-impact-recording
3087
+ *
3088
+ * @description
3089
+ * Extracts every redpanda-docs MCP call and its result from a Claude Code
3090
+ * transcript (the claude-code-action execution file, or `claude -p
3091
+ * --output-format stream-json` output) and writes them as a doc-impact eval
3092
+ * recording. Only the tool name, arguments and result content are kept.
3093
+ * Writes nothing when the session made no docs calls.
3094
+ *
3095
+ * @why
3096
+ * The doc-impact eval replays docs answers per PR. Answers recorded later
3097
+ * come from docs that may already contain the writer's fix, so the eval
3098
+ * would score a correct "already documented" as a miss. The doc-strings
3099
+ * review saves what the docs server told it at review time, and the eval
3100
+ * miner turns that into the item's recording.
3101
+ *
3102
+ * @example
3103
+ * # What the doc-strings review runs after its Claude step
3104
+ * npx doc-tools doc-impact-recording --execution-file "$RUNNER_TEMP/claude-execution-output.json" \
3105
+ * --output recording.json --item redpanda-operator-1615 --server-url https://docs.redpanda.com/mcp
3106
+ */
3107
+ programCli
3108
+ .command('doc-impact-recording')
3109
+ .description('Extract the docs MCP calls from a Claude Code transcript into a doc-impact eval recording')
3110
+ .requiredOption('--execution-file <path>', 'Transcript: a JSON array of SDK messages or stream-json lines')
3111
+ .requiredOption('--output <path>', 'Recording file to write (not written when there are no docs calls)')
3112
+ .option('--item <id>', 'Item id to store in the recording')
3113
+ .option('--server <name>', 'MCP server name in the transcript tool names', 'redpanda-docs')
3114
+ .option('--server-url <url>', 'URL of that MCP server, stored in the recording')
3115
+ .option('--model <name>', 'Model to store (default: the model in the transcript init message)')
3116
+ .option('--recorded-at <iso>', 'Timestamp to store (default: now)')
3117
+ .action((options) => {
3118
+ const { runCli } = require('../tools/doc-impact-recording')
3119
+ process.exit(runCli(options))
3120
+ })
3121
+
2884
3122
  /**
2885
3123
  * lint-screenshots
2886
3124
  *
@@ -2971,6 +3209,113 @@ programCli
2971
3209
  runCli(log, options)
2972
3210
  })
2973
3211
 
3212
+ /**
3213
+ * check-metadata
3214
+ *
3215
+ * @description
3216
+ * Check the taxonomy metadata in AsciiDoc page headers against the docs
3217
+ * taxonomy (taxonomy.yml in the docs repository). On every page,
3218
+ * `:page-categories:` values must resolve to a concept: an unknown value, a
3219
+ * deprecated value, or two values that are the same concept warn, and each
3220
+ * message names the replacement. On a solution overview
3221
+ * (`:page-layout: solution`), `:page-solution-use-cases:`,
3222
+ * `:page-solution-industries:`, `:page-solution-technologies:`, and
3223
+ * `:page-solution-platforms:` must resolve within their own facet, and an
3224
+ * unknown value or a duplicate concept is an error. `:page-topic-type:` is
3225
+ * checked against the content types, as a warning. --strict turns every
3226
+ * warning into an error. The taxonomy comes from --taxonomy (a path or URL),
3227
+ * else the docs checkout at the current directory, else $REDPANDA_DOCS_PATH,
3228
+ * else the published file. The build runs the same rules
3229
+ * (extension-utils/metadata-check.js), so the two never disagree.
3230
+ * --fix first rewrites `:page-categories:` in place: each value becomes its
3231
+ * concept's preferred label, a deprecated value becomes its replacements, and
3232
+ * a replacement that is a task is dropped (the task comes from the module).
3233
+ * Values that need a judgment call (no replacement, or a content type) are
3234
+ * left as they are and still warn.
3235
+ * --suggest also lists, for each page, the topics, use cases, technologies,
3236
+ * and industries its text names that its header does not carry yet, so
3237
+ * authors and agents choose values from the taxonomy instead of inventing
3238
+ * them. Matching is deterministic, with no model: every live concept's
3239
+ * prefLabel and altLabels are matched case-insensitively on whole words
3240
+ * against the title, `:description:`, the page path (module and directories),
3241
+ * section headings, and body, code included and included partials followed.
3242
+ * Words split on anything that is not a letter or digit, so the code literal
3243
+ * `postgres_cdc` names PostgreSQL (altLabel Postgres) and Change data capture
3244
+ * (altLabel CDC). A two-letter label such as Go matches only in its exact
3245
+ * case and never as the first word of a sentence. Hits are weighted title 8,
3246
+ * path 6, description 5, heading 1.5 each (up to 3), and body 1 + log2(n)
3247
+ * (at most 3). A suggestion never names a deprecated concept, never takes a
3248
+ * facet past its cardinality, and skips what the page carries or what that
3249
+ * implies; on a solution overview a technology that a topic implies (Iceberg
3250
+ * Topics implies Apache Iceberg) is suggested in page-solution-technologies.
3251
+ * Each suggestion has a score, a reason, and evidence (the matched term,
3252
+ * where, and the line). --min-score defaults to 5 for Product Docs pages and
3253
+ * 3 for solution overviews; --limit (per facet) to 3 and 6. Suggestions do
3254
+ * not change the exit code.
3255
+ * --suggest also lists candidates: frequent, distinctive terms on a page that
3256
+ * no concept's label or altLabel matches, in any scheme, for proposing new
3257
+ * concepts or altLabels. They are repeated capitalized terms in prose (Amazon
3258
+ * S3, Debezium) and component identifiers (`aws_s3` output, xrefs to Connect
3259
+ * component pages), each with a count, the lines, and a likely facet from a
3260
+ * cue word after it (a connector, database, format, or product word). Terms
3261
+ * in the terminology lists (--terminology, default the docs glossary in the
3262
+ * checkout), common English, and words the page also writes in lowercase are
3263
+ * dropped. --candidates-only skips the check and the suggestions and
3264
+ * aggregates candidates across the pages: one entry per term, with the pages
3265
+ * it is on, leaving out terms on more than a fifth of the pages
3266
+ * (boilerplate).
3267
+ * Exit 0 without errors, 1 with errors, 2 when the check could not run.
3268
+ *
3269
+ * @why
3270
+ * Categories, use cases, and technologies drive search facets and solution
3271
+ * recommendations, and a free-text value becomes a near-duplicate the moment
3272
+ * a second author writes one. This catches it on the PR, without a model, in
3273
+ * a form the docs agents and the PR review can read.
3274
+ *
3275
+ * @example
3276
+ * # Pages changed on this branch (what the docs PR check runs)
3277
+ * npx doc-tools check-metadata --changed origin/main
3278
+ *
3279
+ * # Specific pages or directories, JSON for a grader
3280
+ * npx doc-tools check-metadata modules/manage/pages/iceberg --format json
3281
+ *
3282
+ * # Against a local taxonomy, failing on warnings
3283
+ * npx doc-tools check-metadata --taxonomy ../docs/shared/modules/ROOT/attachments/taxonomy.yml --strict
3284
+ *
3285
+ * # Rewrite deprecated categories to their replacements, then check
3286
+ * npx doc-tools check-metadata --fix modules
3287
+ *
3288
+ * # Suggest taxonomy values for new pages, as JSON for an agent
3289
+ * npx doc-tools check-metadata --suggest --format json modules/manage/pages/new-page.adoc
3290
+ *
3291
+ * # Suggest only for changed pages that carry no categories, with a lower bar
3292
+ * npx doc-tools check-metadata --changed origin/main --suggest --suggest-empty-only --min-score 3
3293
+ *
3294
+ * # Terms across a repository that no concept covers, filtered by the style guide's word lists
3295
+ * npx doc-tools check-metadata --candidates-only --terminology shared/modules/terms/partials,../docs-team-standards/resources/writing-style/terminology.md --format json modules
3296
+ */
3297
+ programCli
3298
+ .command('check-metadata')
3299
+ .description('Check page-categories and solution facet values in AsciiDoc page headers against the docs taxonomy')
3300
+ .argument('[paths...]', 'Files, directories, or globs to check, relative to --root (default: modules/**/*.adoc). With --changed, filters the changed files')
3301
+ .option('--changed <base>', 'Check only .adoc files changed between the merge base with <base> and HEAD, for example origin/main')
3302
+ .option('--taxonomy <path|url>', 'taxonomy.yml to use (default: the docs checkout, $REDPANDA_DOCS_PATH, then https://docs.redpanda.com/shared/_attachments/taxonomy.yml)')
3303
+ .option('--format <format>', 'Output format: text or json', 'text')
3304
+ .option('--strict', 'Treat warnings as errors')
3305
+ .option('--fix', 'Rewrite page-categories in place to current taxonomy labels, then check')
3306
+ .option('--root <path>', 'Repository root (default: current directory)')
3307
+ .option('--output <path>', 'Also write the JSON result to this file')
3308
+ .option('--suggest', 'Also suggest taxonomy values each page does not carry yet, matched from its title, description, path, headings, and body')
3309
+ .option('--min-score <n>', 'With --suggest, the lowest score to suggest (default: 5 on Product Docs pages, 3 on solution overviews)')
3310
+ .option('--limit <n>', 'With --suggest, the most suggestions per facet and page (default: 3 on Product Docs pages, 6 on solution overviews; never past the facet cardinality)')
3311
+ .option('--suggest-empty-only', 'With --suggest, suggest only for pages without :page-categories:')
3312
+ .option('--candidates-only', 'List only candidate terms, frequent distinctive terms that no concept matches, aggregated across the pages with the pages each appears in')
3313
+ .option('--terminology <paths>', 'With --suggest or --candidates-only, comma-separated terminology lists whose terms are never candidates: a glossary partials directory, a Markdown word list, or a text file (default: the docs glossary, shared/modules/terms/partials, in the checkout or $REDPANDA_DOCS_PATH)')
3314
+ .action(async (paths, options) => {
3315
+ const { runCli } = require('../tools/check-metadata')
3316
+ await runCli(paths, options)
3317
+ })
3318
+
2974
3319
  /**
2975
3320
  * check-rendered-html
2976
3321
  *
@@ -3136,7 +3481,7 @@ overridesGroup
3136
3481
  .description('Classify each override field as redundant, upstreamable, or keep against extracted source strings')
3137
3482
  .requiredOption('--overrides <path>', 'Path to the overrides JSON file (for example docs-data/property-overrides.json)')
3138
3483
  .option('--extracted <path>', 'Path to the extracted source JSON (property extractor raw output; required for the properties surface)')
3139
- .addOption(new Option('--surface <surface>', 'Override surface to audit').choices(['properties', 'rpk', 'connect']).default('properties'))
3484
+ .addOption(new Option('--surface <surface>', 'Override surface to audit').choices(['properties', 'rpk']).default('properties'))
3140
3485
  .addOption(new Option('--format <format>', 'Output format').choices(['json', 'human']).default('json'))
3141
3486
  .option('--repo <path>', 'Redpanda checkout to extract raw source strings from (alternative to --extracted)')
3142
3487
  .option('--output <path>', 'Also write the JSON result to this file')
@@ -189,7 +189,7 @@ function generateReviewReport(results, outputPath) {
189
189
  report += `. Regenerate docs\n\n`;
190
190
  } else if (issue.issue.includes('Invalid $ref')) {
191
191
  report += `*Action:*\n\n`;
192
- report += `. Open \`docs-data/overrides.json\`\n`;
192
+ report += `. Open \`${issue.file}\`\n`;
193
193
  report += `. Find the reference at \`${issue.path}\`\n`;
194
194
  report += `. Either add the missing definition or fix the reference\n`;
195
195
  report += `. Regenerate docs\n\n`;
@@ -273,6 +273,17 @@ function generateReviewReport(results, outputPath) {
273
273
  return report;
274
274
  }
275
275
 
276
+ /**
277
+ * Where to fix Redpanda Connect reference text. The connect repo generates
278
+ * the field, component, and Bloblang partials from its specs and the benthos
279
+ * specs, and each Connect release publishes them for the docs build.
280
+ */
281
+ const RPCN_SOURCE_GUIDANCE =
282
+ 'Connector field, component, and Bloblang descriptions are generated from the component specs in ' +
283
+ 'redpanda-data/connect and redpanda-data/benthos. To fix wrong or missing text, open a PR against ' +
284
+ 'the spec in that repository; the fix ships in the next Connect release. Edit hand-written page ' +
285
+ 'prose in rp-connect-docs.';
286
+
276
287
  /**
277
288
  * Review generated documentation for quality issues
278
289
  * @param {Object} args - Arguments
@@ -282,7 +293,7 @@ function generateReviewReport(results, outputPath) {
282
293
  * @param {string} [args.properties_dir] - Custom path to properties directory (default: modules/reference)
283
294
  * @param {string} [args.metrics_file] - Custom path to metrics file (default: modules/reference/pages/public-metrics-reference.adoc)
284
295
  * @param {string} [args.rpk_dir] - Custom path to RPK docs directory (default: autogenerated/{version}/rpk)
285
- * @param {string} [args.overrides_file] - Custom path to overrides.json for RPCN connectors (default: docs-data/overrides.json)
296
+ * @param {string} [args.overrides_file] - Optional connector overrides file to validate for RPCN connectors. Without it, the review points at the connect and benthos specs, where connector descriptions live
286
297
  * @returns {Object} Review results with issues and suggestions
287
298
  */
288
299
  function reviewGeneratedDocs(args) {
@@ -524,16 +535,21 @@ function reviewGeneratedDocs(args) {
524
535
  }
525
536
 
526
537
  case 'rpcn_connectors': {
527
- // Read overrides.json (use custom path or default)
528
- const overridesPath = args.overrides_file ?
529
- path.join(repoRoot.root, args.overrides_file) :
530
- path.join(repoRoot.root, 'docs-data', 'overrides.json');
538
+ // Connector field, component, and Bloblang descriptions are generated
539
+ // from the connect and benthos specs, so there is no overrides file to
540
+ // check by default. Only an overrides file passed explicitly is validated.
541
+ if (!args.overrides_file) {
542
+ suggestions.push(RPCN_SOURCE_GUIDANCE);
543
+ break;
544
+ }
545
+
546
+ const overridesPath = path.join(repoRoot.root, args.overrides_file);
531
547
 
532
548
  if (!fs.existsSync(overridesPath)) {
533
549
  return {
534
550
  success: false,
535
- error: `overrides.json not found at ${overridesPath}`,
536
- suggestion: 'Generate RPCN connector docs first using generate_rpcn_connector_docs tool, or specify custom path with overrides_file parameter'
551
+ error: `Overrides file not found at ${overridesPath}`,
552
+ suggestion: `Check the overrides_file path, or omit it. ${RPCN_SOURCE_GUIDANCE}`
537
553
  };
538
554
  }
539
555
 
@@ -736,5 +752,6 @@ function reviewGeneratedDocs(args) {
736
752
 
737
753
  module.exports = {
738
754
  generateReviewReport,
739
- reviewGeneratedDocs
755
+ reviewGeneratedDocs,
756
+ RPCN_SOURCE_GUIDANCE
740
757
  };
@@ -11,7 +11,7 @@ const { findRepoRoot, getDocToolsCommand, MAX_EXEC_BUFFER_SIZE, DEFAULT_COMMAND_
11
11
  * @param {string} args.overrides - Path to the overrides JSON file (for example "docs-data/property-overrides.json")
12
12
  * @param {string} [args.extracted] - Path to the extracted source JSON (property extractor raw output; required for the properties surface)
13
13
  * @param {string} [args.repo] - Redpanda checkout to extract raw source strings from (alternative to args.extracted, properties surface only)
14
- * @param {string} [args.surface] - Override surface: 'properties', 'rpk', or 'connect'
14
+ * @param {string} [args.surface] - Override surface: 'properties' or 'rpk'
15
15
  * @param {string} [args.output] - Also write the JSON result to this file (relative to repo root)
16
16
  * @returns {Object} Audit results with manifest and per-class summary
17
17
  */
@@ -12,8 +12,6 @@ const { execSync, spawnSync } = require('child_process');
12
12
  const { findRepoRoot, MAX_EXEC_BUFFER_SIZE, DEFAULT_COMMAND_TIMEOUT } = require('./utils');
13
13
  const { getAntoraStructure } = require('./antora');
14
14
  const cache = require('./cache');
15
- const fs = require('fs');
16
- const path = require('path');
17
15
 
18
16
  /**
19
17
  * Generate Redpanda Connect connector documentation
@@ -59,19 +57,11 @@ function generateRpConnectDocs(args = {}) {
59
57
  cmdArgs.push(args.csv);
60
58
  }
61
59
 
62
- // Default to using overrides.json from docs-data if it exists
63
- let overridesPath = args.overrides;
64
- if (!overridesPath) {
65
- // Try to find docs-data/overrides.json in current directory
66
- const defaultOverrides = path.join(process.cwd(), 'docs-data', 'overrides.json');
67
- if (fs.existsSync(defaultOverrides)) {
68
- overridesPath = defaultOverrides;
69
- }
70
- }
71
-
72
- if (overridesPath) {
60
+ // Overrides are opt-in. Connector descriptions come from the connect and
61
+ // benthos specs, so no file is picked up implicitly.
62
+ if (args.overrides) {
73
63
  cmdArgs.push('--overrides');
74
- cmdArgs.push(overridesPath);
64
+ cmdArgs.push(args.overrides);
75
65
  }
76
66
  if (args.cloud_version) {
77
67
  cmdArgs.push('--cloud-version');