@redpanda-data/docs-extensions-and-macros 5.35.1 → 5.37.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
@@ -2312,6 +2312,107 @@ automation
2312
2312
  }
2313
2313
  })
2314
2314
 
2315
+ /**
2316
+ * generate kapa-source-groups
2317
+ *
2318
+ * @description
2319
+ * Reads the source-group tree and the per-source group assignments from Kapa's ingestion API
2320
+ * and writes a mapping from each docs URL version segment to the Kapa source group that scopes
2321
+ * retrieval to that version. Consumers pass the group id as `sourceGroupIdsInclude` (Agent SDK),
2322
+ * `sourceGroupIDsInclude` (Chat SDK, note the capital ID) or `_meta.source_group_ids_include`
2323
+ * (hosted MCP server).
2324
+ *
2325
+ * The mapping is keyed on the URL segment, not Antora's page.version, because the two disagree
2326
+ * for the latest release: latest_version_segment: 'current' publishes 26.2 at
2327
+ * /streaming/current/ while page.version reads 26.2.
2328
+ *
2329
+ * This command only ever reads from Kapa. Kapa publishes no write API ("we do not provide an API
2330
+ * endpoint for uploading or managing sources"), so the groups and their source assignments are
2331
+ * created by hand in the Kapa dashboard under Sources > Manage groups. Run this afterwards to
2332
+ * record what is there.
2333
+ *
2334
+ * Refuses rather than writing a misleading mapping when: no groups exist, no group has children,
2335
+ * any version group has no sources assigned, or --default-segment is not one of the groups. An
2336
+ * empty version group is worse than no group, because scoping a query to it returns only the
2337
+ * global sources and the reader silently gets no version-specific content.
2338
+ *
2339
+ * @why
2340
+ * Kapa indexes one separately crawled source per published docs version. Without retrieval
2341
+ * scoping a reader gets answers drawn from any of them: a measured call for "What is a Redpanda
2342
+ * topic partition?" returned 12 sections, six of which were the same get-started/architecture
2343
+ * page at six different versions. Source groups fix that at retrieval time, but the group UUIDs
2344
+ * live in Kapa while the version list lives in Antora, so something has to join the two. Because
2345
+ * a new docs version appears with no file change in docs-site (the playbook globs branches: v/*),
2346
+ * a hand-maintained mapping goes stale silently; generating it and diff-checking the result in CI
2347
+ * is what makes drift loud.
2348
+ *
2349
+ * @example
2350
+ * # Write the mapping to its default location
2351
+ * export KAPA_API_KEY=... KAPA_PROJECT_ID=...
2352
+ * npx doc-tools generate kapa-source-groups
2353
+ *
2354
+ * # Preview without writing
2355
+ * npx doc-tools generate kapa-source-groups --dry-run
2356
+ *
2357
+ * # Scope unversioned pages to something other than "current"
2358
+ * npx doc-tools generate kapa-source-groups --default-segment 26.1
2359
+ *
2360
+ * # Disambiguate when more than one group has children
2361
+ * npx doc-tools generate kapa-source-groups --parent-group Streaming
2362
+ *
2363
+ * @requirements
2364
+ * - Kapa API key and project ID, set via KAPA_API_KEY and KAPA_PROJECT_ID
2365
+ * - Create the key in the Kapa platform under Configuration > API Keys; the project ID is the
2366
+ * UUID in the dashboard URL (https://app.kapa.ai/<project-id>)
2367
+ * - The parent group and its version sub groups must already exist in Kapa, with each version
2368
+ * group holding at least one source
2369
+ * - Internet connection to reach https://api.kapa.ai
2370
+ */
2371
+ automation
2372
+ .command('kapa-source-groups')
2373
+ .description('Generate the docs version segment to Kapa source group mapping from Kapa\'s ingestion API')
2374
+ .option('--output <file>', 'Output file (relative to repo root, must stay inside the repository)', 'docs-data/kapa-source-groups.json')
2375
+ .option('--default-segment <segment>', 'Segment used for pages with no version of their own', 'current')
2376
+ .option('--parent-group <name>', 'Restrict to one parent group by name (default: the only group that has children)')
2377
+ .option('--dry-run', 'Print output to stdout instead of writing file')
2378
+ .action(async (options) => {
2379
+ const { generateKapaSourceGroups } = require('../tools/kapa-source-groups/generate-kapa-source-groups.js')
2380
+ const { requireKapaCredentials } = require('../cli-utils/kapa-credentials')
2381
+
2382
+ try {
2383
+ const repoRoot = findRepoRoot()
2384
+ // resolveInsideRepo for the same reason every other generator uses it: this
2385
+ // command is reachable from the MCP server, where a write option that escapes
2386
+ // the repo is a write-anywhere primitive.
2387
+ const absOutput = options.dryRun ? undefined : resolveInsideRepo(repoRoot, options.output, '--output')
2388
+
2389
+ // Fail loud on a missing credential rather than emitting an empty mapping:
2390
+ // unauthenticated would look identical to "Kapa has no source groups", and the
2391
+ // drift check would then report false drift forever.
2392
+ const { apiKey, projectId } = requireKapaCredentials()
2393
+
2394
+ const out = await generateKapaSourceGroups({
2395
+ apiKey,
2396
+ projectId,
2397
+ defaultSegment: options.defaultSegment,
2398
+ parentGroupName: options.parentGroup,
2399
+ })
2400
+
2401
+ if (options.dryRun) {
2402
+ process.stdout.write(out)
2403
+ return
2404
+ }
2405
+
2406
+ fs.mkdirSync(path.dirname(absOutput), { recursive: true })
2407
+ fs.writeFileSync(absOutput, out, 'utf8')
2408
+ const segments = Object.keys(JSON.parse(out).segments)
2409
+ console.log(`Done: Wrote ${absOutput} (${segments.length} version segments: ${segments.join(', ')})`)
2410
+ } catch (err) {
2411
+ console.error(`Error: Failed to generate Kapa source groups: ${err.message}`)
2412
+ process.exit(1)
2413
+ }
2414
+ })
2415
+
2315
2416
  const validation = new Command('validate').description('Validate docs data against internal sources of truth')
2316
2417
 
2317
2418
  /**
@@ -2734,6 +2835,167 @@ overridesGroup
2734
2835
  })
2735
2836
 
2736
2837
  programCli.addCommand(automation)
2838
+ /**
2839
+ * validate kapa-source-groups
2840
+ *
2841
+ * @description
2842
+ * Checks that every streaming docs version currently published has a Kapa source, so Ask AI
2843
+ * can scope answers to it. Exit status is meaningful: 0 every version is covered, 1 a version
2844
+ * is missing, 2 the command itself failed (Kapa or the sitemap unreachable, bad credentials).
2845
+ *
2846
+ * A version counts as covered when Kapa has a source named `Documentation (X)` for it, that
2847
+ * source is assigned to a source group, AND the committed mapping (docs-data/kapa-source-groups.json)
2848
+ * has a segment for it whose group_id is a group that source actually sits in. Four gaps are
2849
+ * reported, each with its own line:
2850
+ *
2851
+ * - missing: the version is published but Kapa has no `Documentation (X)` source.
2852
+ * - unassigned: the source exists but is in no group, so it is global in Kapa and returned for
2853
+ * EVERY query on every version.
2854
+ * - unmapped: Kapa has the grouped source but the committed mapping has no segment for it. Every
2855
+ * deployed surface reads the mapping, not Kapa, so readers on that version silently get the
2856
+ * default segment until the mapping is regenerated and released.
2857
+ * - mismatched: the mapping has the segment but its group_id is not a group the version's source
2858
+ * sits in, which is what a group deleted and recreated in the dashboard, or a source moved into
2859
+ * another version's group, looks like. Every Ask AI query on that version is then scoped to a
2860
+ * group that no longer holds its docs, until the mapping is regenerated and released.
2861
+ *
2862
+ * The prerelease segment (/streaming/beta/) is ignored: a beta publishes there for the whole
2863
+ * pre-GA cycle and is not expected to have a Kapa group of its own.
2864
+ *
2865
+ * @why
2866
+ * The latest release always publishes at /streaming/current/, and the `Documentation (current)`
2867
+ * crawl follows it, so latest is never the version that goes missing. The one that goes missing
2868
+ * is the version that just got archived: when 26.3 ships, 26.2 moves to /streaming/26.2/ and
2869
+ * needs a crawl of its own that nobody has made yet. Kapa has no write API, so that crawl is
2870
+ * created by hand in the dashboard, and nothing else notices when it is forgotten. Readers on the
2871
+ * archived version then silently fall back to the default segment.
2872
+ *
2873
+ * Run it on a schedule so the gap opens an issue rather than waiting for a bug report.
2874
+ *
2875
+ * @example
2876
+ * export KAPA_API_KEY=... KAPA_PROJECT_ID=...
2877
+ * npx doc-tools validate kapa-source-groups
2878
+ *
2879
+ * # Check a preview or staging site instead of production
2880
+ * npx doc-tools validate kapa-source-groups --site-url https://deploy-preview-123--redpanda-documentation.netlify.app
2881
+ *
2882
+ * @requirements
2883
+ * - Kapa API key and project ID, set via KAPA_API_KEY and KAPA_PROJECT_ID
2884
+ * - Internet connection to reach https://api.kapa.ai and the docs site's sitemap
2885
+ */
2886
+ validation
2887
+ .command('kapa-source-groups')
2888
+ .description('Check every published streaming version has a grouped Kapa source and the committed mapping points at that group (exit 0 in sync, 1 gap, 2 error)')
2889
+ .option('--site-url <url>', 'Docs site whose sitemap says which versions are published', 'https://docs.redpanda.com')
2890
+ .option('--mapping <file>', 'Committed mapping every surface reads', 'docs-data/kapa-source-groups.json')
2891
+ .action(async (options) => {
2892
+ const { fetchKapaSourceVersions } = require('../tools/kapa-source-groups/kapa-source-versions.js')
2893
+ const { fetchPublishedSegments, isPrereleaseSegment } = require('../tools/kapa-source-groups/published-segments.js')
2894
+ const { requireKapaCredentials } = require('../cli-utils/kapa-credentials')
2895
+
2896
+ // Printed only where a gap has actually been established, and required by
2897
+ // kapa-source-groups-drift.sh before it files an issue. Exit 1 alone is not
2898
+ // trustworthy: Commander exits 1 on a usage error and an uncaught throw
2899
+ // exits 1 too, so without this a typo'd flag would file an issue quoting a
2900
+ // stack trace as the report.
2901
+ const SENTINEL = 'KAPA_DRIFT_CONFIRMED'
2902
+
2903
+ // Exit 2 is reserved for "could not find out", so the scheduled job can tell
2904
+ // "a version is missing" apart from "Kapa was down" and not file a false issue.
2905
+ let published, kapa
2906
+ try {
2907
+ const { apiKey, projectId } = requireKapaCredentials()
2908
+ ;[published, kapa] = await Promise.all([
2909
+ fetchPublishedSegments({ siteUrl: options.siteUrl }),
2910
+ fetchKapaSourceVersions({ apiKey, projectId }),
2911
+ ])
2912
+ } catch (err) {
2913
+ console.error(`Error: Could not check Kapa sources: ${err.message}`)
2914
+ process.exit(2)
2915
+ }
2916
+
2917
+ // The committed mapping is what every surface actually reads (the Antora
2918
+ // extension, docs-ui, the /api/ proxy, the MCP server). Kapa having a
2919
+ // grouped source is necessary but not sufficient: if nobody regenerated and
2920
+ // released the mapping, readers on that version still get the default
2921
+ // segment and nothing else would ever say so.
2922
+ let mappedSegments, mappedGroupIds
2923
+ try {
2924
+ const mappingPath = path.resolve(options.mapping)
2925
+ const mapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'))
2926
+ if (!mapping || typeof mapping.segments !== 'object' || mapping.segments === null) {
2927
+ throw new Error('no segments object')
2928
+ }
2929
+ mappedSegments = new Set(Object.keys(mapping.segments))
2930
+ mappedGroupIds = new Map(Object.entries(mapping.segments)
2931
+ .map(([seg, entry]) => [seg, entry && entry.group_id != null ? String(entry.group_id) : null]))
2932
+ } catch (err) {
2933
+ console.error(`Error: Could not read the committed mapping at ${options.mapping}: ${err.message}`)
2934
+ process.exit(2)
2935
+ }
2936
+
2937
+ // A beta publishes at /streaming/beta/ for the whole pre-GA cycle and has no
2938
+ // group of its own by design. Reporting it would file a false issue weekly.
2939
+ const prerelease = published.filter(isPrereleaseSegment)
2940
+ const versions = published.filter((v) => !isPrereleaseSegment(v))
2941
+
2942
+ const missing = versions.filter((v) => !kapa.covered.has(v) && !kapa.unassigned.has(v))
2943
+ const unassigned = versions.filter((v) => kapa.unassigned.has(v))
2944
+ const unmapped = versions.filter((v) => kapa.covered.has(v) && !mappedSegments.has(v))
2945
+ // The mapping is what gets sent with every query, so its group_id has to be a
2946
+ // group the version's source is in right now. A group deleted and recreated
2947
+ // keeps its name and gets a new id; a source moved to another version's
2948
+ // group leaves the old id pointing at a group without these docs. Kapa
2949
+ // reports neither, and the three checks above pass in both cases.
2950
+ const mismatched = versions.filter((v) => {
2951
+ if (!kapa.covered.has(v) || !mappedSegments.has(v)) return false
2952
+ const want = mappedGroupIds.get(v)
2953
+ const have = kapa.groups.get(v) || new Set()
2954
+ return !want || !have.has(want)
2955
+ })
2956
+
2957
+ if (prerelease.length) {
2958
+ console.log(` (ignoring prerelease segment${prerelease.length > 1 ? 's' : ''} ${prerelease.join(', ')}, ` +
2959
+ 'which publishes for the pre-GA cycle and is not expected to have a Kapa group)')
2960
+ }
2961
+
2962
+ if (missing.length === 0 && unassigned.length === 0 && unmapped.length === 0 && mismatched.length === 0) {
2963
+ console.log(`✓ Every published streaming version (${versions.join(', ')}) has a Kapa source in a group, ` +
2964
+ `and ${options.mapping} points each one at that group.`)
2965
+ process.exit(0)
2966
+ }
2967
+
2968
+ console.log('✗ Kapa and the committed mapping are out of sync for published streaming docs versions.\n')
2969
+ for (const v of missing) {
2970
+ console.log(` - ${v}: published at /streaming/${v}/ but Kapa has no "Documentation (${v})" source. ` +
2971
+ 'Readers on this version fall back to the default segment.')
2972
+ }
2973
+ for (const v of unassigned) {
2974
+ console.log(` - ${v}: "Documentation (${v})" exists but is not in a source group, so it is global ` +
2975
+ 'and returned for every query on every version.')
2976
+ }
2977
+ for (const v of unmapped) {
2978
+ console.log(` - ${v}: Kapa has a grouped "Documentation (${v})" source, but ${options.mapping} has no ` +
2979
+ `"${v}" segment. Readers on this version fall back to the default segment until the mapping is ` +
2980
+ 'regenerated and released.')
2981
+ }
2982
+ for (const v of mismatched) {
2983
+ const want = mappedGroupIds.get(v)
2984
+ const have = [...(kapa.groups.get(v) || [])]
2985
+ console.log(` - ${v}: ${options.mapping} scopes this version to group ${want || '(no group_id)'}, but Kapa has ` +
2986
+ `"Documentation (${v})" in ${have.length === 1 ? 'group' : 'groups'} ${have.join(', ')}. Every Ask AI query on ` +
2987
+ 'this version is sent with a group that no longer holds its docs until the mapping is regenerated and released.')
2988
+ }
2989
+ if (missing.length || unassigned.length) {
2990
+ console.log('\nIn the Kapa dashboard: Sources > Add source for the crawl, then assign it to the version ' +
2991
+ 'group under Sources > Manage groups. Then regenerate the mapping:\n doc-tools generate kapa-source-groups')
2992
+ } else {
2993
+ console.log('\nRegenerate the mapping and release docs-extensions-and-macros:\n doc-tools generate kapa-source-groups')
2994
+ }
2995
+ console.log(`\n${SENTINEL}`)
2996
+ process.exit(1)
2997
+ })
2998
+
2737
2999
  programCli.addCommand(validation)
2738
3000
  programCli.addCommand(overridesGroup)
2739
3001
  programCli.parse(process.argv)
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Kapa API Credentials Utility
3
+ *
4
+ * Provides a consistent way to retrieve Kapa API credentials from environment
5
+ * variables, so no generator reads process.env.KAPA_* directly. Mirrors
6
+ * cli-utils/github-token.js: resolve here, return null when absent, and let the
7
+ * calling command decide whether a missing credential is fatal.
8
+ *
9
+ * The variable names are not new. They are already the established pair in
10
+ * redpanda-data/docs-site (netlify/functions/kapa-session.mjs) and in
11
+ * docs-team-standards/scripts/docs_analytics/kapa_probe.py, so a third spelling
12
+ * would just be a fourth place to get it wrong.
13
+ */
14
+
15
+ // Kapa's public API. Both the ingestion and query endpoints live here, and both
16
+ // authenticate with the same project API key -- there is no separate ingestion
17
+ // key and no scope model.
18
+ const KAPA_API_BASE = 'https://api.kapa.ai';
19
+
20
+ // Kapa's OpenAPI schema spells the header X-API-KEY; their prose pages also use
21
+ // X-API-Key in places. HTTP header names are case-insensitive so either works,
22
+ // but pin the canonical spelling from the schema.
23
+ const KAPA_API_KEY_HEADER = 'X-API-KEY';
24
+
25
+ /**
26
+ * Get the Kapa API key.
27
+ *
28
+ * @param {object} [env] - Environment to read (defaults to process.env)
29
+ * @returns {string|null} The API key, or null when unset/blank
30
+ */
31
+ function getKapaApiKey (env = process.env) {
32
+ const key = (env.KAPA_API_KEY || '').trim()
33
+ return key || null
34
+ }
35
+
36
+ /**
37
+ * Get the Kapa project ID.
38
+ *
39
+ * @param {object} [env] - Environment to read (defaults to process.env)
40
+ * @returns {string|null} The project ID, or null when unset/blank
41
+ */
42
+ function getKapaProjectId (env = process.env) {
43
+ const id = (env.KAPA_PROJECT_ID || '').trim()
44
+ return id || null
45
+ }
46
+
47
+ /**
48
+ * True when both credentials are present.
49
+ *
50
+ * @param {object} [env] - Environment to read (defaults to process.env)
51
+ * @returns {boolean}
52
+ */
53
+ function hasKapaCredentials (env = process.env) {
54
+ return Boolean(getKapaApiKey(env) && getKapaProjectId(env))
55
+ }
56
+
57
+ /**
58
+ * Resolve both credentials, or throw with a remediation hint naming exactly
59
+ * what is missing.
60
+ *
61
+ * Generators that write published content call this and let it throw: an
62
+ * unauthenticated run would otherwise produce an empty mapping that looks
63
+ * identical to "Kapa has no source groups", and the drift check would then
64
+ * report false drift forever. Fail loud, like cloud-regions does for its
65
+ * GitHub token.
66
+ *
67
+ * @param {object} [env] - Environment to read (defaults to process.env)
68
+ * @returns {{apiKey: string, projectId: string, apiBase: string, headerName: string}}
69
+ * @throws {Error} When either credential is missing
70
+ */
71
+ function requireKapaCredentials (env = process.env) {
72
+ const apiKey = getKapaApiKey(env)
73
+ const projectId = getKapaProjectId(env)
74
+
75
+ const missing = []
76
+ if (!apiKey) missing.push('KAPA_API_KEY')
77
+ if (!projectId) missing.push('KAPA_PROJECT_ID')
78
+
79
+ if (missing.length) {
80
+ throw new Error(
81
+ `Kapa credentials are required: ${missing.join(' and ')} ${missing.length > 1 ? 'are' : 'is'} not set. ` +
82
+ 'Create an API key in the Kapa platform under Configuration > API Keys, and find the project ID ' +
83
+ 'in the dashboard URL (https://app.kapa.ai/<project-id>). ' +
84
+ 'In CI these come from AWS Secrets Manager via OIDC, not from GitHub secrets directly.'
85
+ )
86
+ }
87
+
88
+ return { apiKey, projectId, apiBase: KAPA_API_BASE, headerName: KAPA_API_KEY_HEADER }
89
+ }
90
+
91
+ /**
92
+ * Build the auth headers for a Kapa API request.
93
+ *
94
+ * @param {string} apiKey
95
+ * @returns {object} Headers suitable for fetch()
96
+ */
97
+ function kapaAuthHeaders (apiKey) {
98
+ return { [KAPA_API_KEY_HEADER]: apiKey, Accept: 'application/json' }
99
+ }
100
+
101
+ module.exports = {
102
+ KAPA_API_BASE,
103
+ KAPA_API_KEY_HEADER,
104
+ getKapaApiKey,
105
+ getKapaProjectId,
106
+ hasKapaCredentials,
107
+ requireKapaCredentials,
108
+ kapaAuthHeaders,
109
+ }
@@ -0,0 +1,119 @@
1
+ {
2
+ "project_id": "97f44223-f930-4fb9-ae1e-ecd436a4d85c",
3
+ "parent_group": {
4
+ "id": "238b3c08-5cf6-4615-937c-fa2f5a2e83a4",
5
+ "name": "Streaming",
6
+ "type": "product"
7
+ },
8
+ "default_segment": "current",
9
+ "segments": {
10
+ "23.3": {
11
+ "group_id": "625e5538-aa73-4969-a5c1-4c35a0108a94",
12
+ "group_name": "23.3",
13
+ "source_ids": [
14
+ "21672ac9-cc94-4529-8835-55334ede0a18"
15
+ ],
16
+ "source_names": [
17
+ "Documentation (23.3)"
18
+ ]
19
+ },
20
+ "24.1": {
21
+ "group_id": "29357fc1-30b1-46d4-a9de-e30f9b696b2f",
22
+ "group_name": "24.1",
23
+ "source_ids": [
24
+ "0d68df8c-1f9b-45f6-baa3-25d8bb46b164"
25
+ ],
26
+ "source_names": [
27
+ "Documentation (24.1)"
28
+ ]
29
+ },
30
+ "24.2": {
31
+ "group_id": "3442b9cb-67db-4800-a320-9def40551b93",
32
+ "group_name": "24.2",
33
+ "source_ids": [
34
+ "11659ad5-b7df-4a06-9e8b-4e9913795889"
35
+ ],
36
+ "source_names": [
37
+ "Documentation (24.2)"
38
+ ]
39
+ },
40
+ "24.3": {
41
+ "group_id": "11288265-8a9e-4a49-8c63-2f9fbe459b76",
42
+ "group_name": "24.3",
43
+ "source_ids": [
44
+ "f1377953-8bcc-48c9-a0e7-acb8efae701e"
45
+ ],
46
+ "source_names": [
47
+ "Documentation (24.3)"
48
+ ]
49
+ },
50
+ "25.1": {
51
+ "group_id": "317edba8-6c78-4cca-bad5-1dfe47c9d3dc",
52
+ "group_name": "25.1",
53
+ "source_ids": [
54
+ "6cfe84a2-4d5b-4123-b302-f372a5500d02"
55
+ ],
56
+ "source_names": [
57
+ "Documentation (25.1)"
58
+ ]
59
+ },
60
+ "25.2": {
61
+ "group_id": "639427c4-06fb-4b8f-8df3-d6a444b85101",
62
+ "group_name": "25.2",
63
+ "source_ids": [
64
+ "4d4d1a66-08e7-46aa-ac72-fea301f96b5f"
65
+ ],
66
+ "source_names": [
67
+ "Documentation (25.2)"
68
+ ]
69
+ },
70
+ "25.3": {
71
+ "group_id": "65723bc1-941c-4b3b-b5ba-2c601f7b3ba9",
72
+ "group_name": "25.3",
73
+ "source_ids": [
74
+ "d1213d06-6132-47e2-abca-43fe0a85dde6"
75
+ ],
76
+ "source_names": [
77
+ "Documentation (25.3)"
78
+ ]
79
+ },
80
+ "26.1": {
81
+ "group_id": "f0751d2d-de07-4a9c-8d75-7960121178b7",
82
+ "group_name": "26.1",
83
+ "source_ids": [
84
+ "ec6810ab-6208-4e6b-acef-83da55d45762"
85
+ ],
86
+ "source_names": [
87
+ "Documentation (26.1)"
88
+ ]
89
+ },
90
+ "current": {
91
+ "group_id": "35567e94-30c7-4608-ab5d-1ff982339d05",
92
+ "group_name": "current",
93
+ "source_ids": [
94
+ "f5c1e522-be3a-45f6-8463-11abc1b35ea9"
95
+ ],
96
+ "source_names": [
97
+ "Documentation (current)"
98
+ ]
99
+ }
100
+ },
101
+ "global_sources": [
102
+ "Admin API",
103
+ "Agentic Data Plane",
104
+ "Cloud Controlplane API",
105
+ "Cloud Dataplane API",
106
+ "Corrected Responses",
107
+ "Documentation (Cloud)",
108
+ "Documentation (RPCN)",
109
+ "GitHub Discussions",
110
+ "HTTP Proxy API",
111
+ "pylon",
112
+ "Recruitment Scam Page",
113
+ "Redpanda Connect Release Notes",
114
+ "Redpanda Core Release Notes",
115
+ "Schema Registry API",
116
+ "Terraform provider",
117
+ "YouTube Tutorials"
118
+ ]
119
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://redpanda.com/schemas/kapa-source-groups.schema.json",
4
+ "title": "Kapa source groups",
5
+ "description": "Generated mapping from a docs URL version segment to the Kapa source group that scopes retrieval to that version. Produced by `doc-tools generate kapa-source-groups` from Kapa's ingestion API; do not hand-edit. Kapa publishes no write API, so the groups and their source assignments are created in the Kapa dashboard and this file only records what is there. See tools/kapa-source-groups/generate-kapa-source-groups.js.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["project_id", "parent_group", "default_segment", "segments", "global_sources"],
9
+ "properties": {
10
+ "project_id": {
11
+ "type": "string",
12
+ "format": "uuid",
13
+ "description": "Kapa project the groups belong to. Matches KAPA_PROJECT_ID. Recorded so a mapping generated against the wrong project is obvious rather than silently wrong."
14
+ },
15
+ "parent_group": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": ["id", "name", "type"],
19
+ "description": "The product-level group the version groups hang off. Kapa's hierarchy is two levels and is expressed top-down via sub_groups; there is no parent_id field anywhere in its schema, so the generator finds this by 'the group that has children' rather than by name. Renaming it in the dashboard is therefore safe.",
20
+ "properties": {
21
+ "id": { "type": "string", "format": "uuid" },
22
+ "name": { "type": "string" },
23
+ "type": {
24
+ "type": ["string", "null"],
25
+ "enum": ["product", "version", null],
26
+ "description": "Kapa requires every group to be typed. The enum appears only in Kapa's API schema, not in any prose doc. Nothing in Kapa's documented retrieval semantics reads it, so it is recorded for humans rather than relied on."
27
+ }
28
+ }
29
+ },
30
+ "default_segment": {
31
+ "type": "string",
32
+ "description": "Segment used for any page with no version of its own: Cloud, Connect, Agentic Data Plane, labs, home, search, the 404 page, and the Bump-rendered /api/ pages. Must be a key of `segments`. Deliberately set rather than left unset: sending no group filter makes Kapa search every version at once, which is the DOC-2450 failure (a reader on an unversioned page was answered from 25.2 instead of current)."
33
+ },
34
+ "segments": {
35
+ "type": "object",
36
+ "minProperties": 1,
37
+ "description": "Keyed by the docs URL version segment, not by the Antora `page.version` attribute. The two disagree for the latest release: `latest_version_segment: 'current'` publishes 26.2 at /streaming/current/ while page.version reads 26.2. Kapa source_url values and the URL prefixes in docs-site's lib/platform-filter.mjs both use the segment, so that is what this keys on.",
38
+ "additionalProperties": { "$ref": "#/$defs/segment" }
39
+ },
40
+ "global_sources": {
41
+ "type": "array",
42
+ "items": { "type": "string" },
43
+ "description": "Names of the Kapa sources deliberately assigned to no group. These are the 'global' set: always returned alongside whichever group a query scopes to. Recorded because the design depends on it: Agentic Data Plane, Cloud and Connect content must stay reachable from every page, whatever version the reader is on. Note 'Global' is NOT a real Kapa group despite the dashboard showing a Global row: the API returns no such object and an unassigned source reports source_groups: []."
44
+ }
45
+ },
46
+ "$defs": {
47
+ "segment": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["group_id", "group_name", "source_ids", "source_names"],
51
+ "properties": {
52
+ "group_id": {
53
+ "type": "string",
54
+ "format": "uuid",
55
+ "description": "The value to pass as sourceGroupIdsInclude (Agent SDK), sourceGroupIDsInclude (Chat SDK, note the capital ID), or _meta.source_group_ids_include (hosted MCP server). Kapa group IDs are stable across renames."
56
+ },
57
+ "group_name": { "type": "string" },
58
+ "source_ids": {
59
+ "type": "array",
60
+ "minItems": 1,
61
+ "items": { "type": "string", "format": "uuid" },
62
+ "description": "Sources assigned to this group. minItems is 1 on purpose: a version group with no sources is worse than no group at all, because scoping to it returns only the global set and the reader silently gets nothing version-specific. The generator refuses to emit such a mapping."
63
+ },
64
+ "source_names": {
65
+ "type": "array",
66
+ "minItems": 1,
67
+ "items": { "type": "string" },
68
+ "description": "Human-readable counterpart to source_ids, so a reviewer can sanity-check the mapping without opening the Kapa dashboard."
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }