Skip to content

Commit 528872c

Browse files
committed
feat: add experimental map command
1 parent 51b1d15 commit 528872c

31 files changed

Lines changed: 3467 additions & 1 deletion

File tree

docs/@v2/commands/map.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# `map`
2+
3+
## Introduction
4+
5+
The `map` command generates a structural map of an API description:
6+
a hierarchical tree that mirrors the document, like a sitemap for API tooling and agents.
7+
Every node in the tree represents a retrievable section of the description — an operation, a channel, a named component, a webhook, a server, or a tag — and is addressed by a canonical JSON pointer.
8+
9+
{% admonition type="warning" name="OpenAPI and AsyncAPI only" %}
10+
The `map` command is considered an experimental feature.
11+
This means it's still a work in progress and may go through major changes.
12+
13+
It supports OpenAPI 2.0 through 3.2 and AsyncAPI 2.x and 3.0 descriptions.
14+
{% /admonition %}
15+
16+
The API map is designed as a compact index that tools — including LLM-based agents — can navigate to decide which parts of an API description to retrieve,
17+
instead of processing the whole document.
18+
The tree structure and node summaries are extracted deterministically from the description itself; no external services are involved.
19+
20+
Each node has the following fields:
21+
22+
| Field | Type | Description |
23+
| ------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24+
| title | string | Human-readable name: the API title, path, `operationId`, channel address, component name, tag name, or server URL. |
25+
| kind | string | Node type name from Redocly's internal type system, for example `Root`, `Info`, `Server`, `Tag`, `Paths`, `PathItem`, `Operation`, `Channel`, `Components`, `NamedSchemas`, or `Schema`. |
26+
| pointer | string | Canonical JSON pointer that identifies and addresses the node, for example `#/paths/~1menu/get`. |
27+
| summary | string | Optional. The node's `summary` field, or its `description` truncated at a word boundary (about 200 characters). |
28+
| method | string | Optional. HTTP method; present on OpenAPI `Operation` nodes only. |
29+
| path | string | Optional. URL path; present on OpenAPI `Operation` nodes under `paths` only. |
30+
| source | object | Optional. Original `{ file, pointer }` location of the node; present when `--source-locations` is used. |
31+
| nodes | array | Child nodes. |
32+
33+
The tree stops at operations, channels, and named components;
34+
parameters, responses, messages payloads, and schema internals are the node's content, retrievable through its pointer.
35+
36+
{% admonition type="info" name="Pointers are logical" %}
37+
Pointers address the logical document structure as authored.
38+
For multi-file descriptions, note that `redocly bundle` may store referenced components under different keys;
39+
use `--source-locations` when you need the exact file and location of each node.
40+
{% /admonition %}
41+
42+
## Usage
43+
44+
```bash
45+
redocly map <api>
46+
redocly map <api> [--format=<option>] [--source-locations] [--config=<path>]
47+
redocly map --version
48+
```
49+
50+
## Options
51+
52+
| Option | Type | Description |
53+
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
54+
| api | string | **REQUIRED.** Path to the API description filename or alias that you want to generate the map for. |
55+
| --config | string | Specify path to the [configuration file](../configuration/index.md). |
56+
| --format | string | Format for the output.<br />**Possible values:** `stylish`, `json`. Default value is `stylish`. |
57+
| --help | boolean | Show help. |
58+
| --lint-config | string | Specify the severity level for the configuration file. <br/> **Possible values:** `warn`, `error`, `off`. Default value is `warn`. |
59+
| --source-locations | boolean | Include the original source file and pointer for each node. Useful for multi-file descriptions. |
60+
| --version | boolean | Show version number. |
61+
62+
## Examples
63+
64+
### Map an OpenAPI description (stylish, default format)
65+
66+
The default output format prints an indented tree, one node per line, with the node title, kind, and pointer:
67+
68+
```
69+
Redocly Cafe Root #/
70+
Redocly Cafe Info #/info
71+
paths Paths #/paths
72+
/menu PathItem #/paths/~1menu
73+
listMenuItems Operation #/paths/~1menu/get
74+
createMenuItem Operation #/paths/~1menu/post
75+
components Components #/components
76+
schemas NamedSchemas #/components/schemas
77+
MenuItem Schema #/components/schemas/MenuItem
78+
```
79+
80+
### Map an AsyncAPI description
81+
82+
AsyncAPI descriptions map to their own structure: channels, operations (AsyncAPI 3), and message components:
83+
84+
```
85+
Account Service Root #/
86+
Account Service Info #/info
87+
channels NamedChannels #/channels
88+
userSignedup Channel #/channels/userSignedup
89+
components Components #/components
90+
messages NamedMessages #/components/messages
91+
UserSignedUp Message #/components/messages/UserSignedUp
92+
operations NamedOperations #/operations
93+
sendUserSignedup Operation #/operations/sendUserSignedup
94+
```
95+
96+
### Generate a machine-readable map
97+
98+
Use `--format=json` to get the map as a JSON tree, suitable for further processing:
99+
100+
```bash
101+
redocly map openapi.yaml --format=json
102+
```
103+
104+
```json
105+
{
106+
"title": "Redocly Cafe",
107+
"kind": "Root",
108+
"pointer": "#/",
109+
"nodes": [
110+
{
111+
"title": "listMenuItems",
112+
"kind": "Operation",
113+
"pointer": "#/paths/~1menu/get",
114+
"summary": "List all menu items",
115+
"method": "get",
116+
"path": "/menu",
117+
"nodes": []
118+
}
119+
]
120+
}
121+
```
122+
123+
### Include source locations
124+
125+
For descriptions split across multiple files with `$ref`s,
126+
use `--source-locations` to add the original file and pointer to every node:
127+
128+
```bash
129+
redocly map openapi.yaml --format=json --source-locations
130+
```
131+
132+
```json
133+
{
134+
"title": "/menu",
135+
"kind": "PathItem",
136+
"pointer": "#/paths/~1menu",
137+
"source": {
138+
"file": "paths/menu.yaml",
139+
"pointer": "#/"
140+
},
141+
"nodes": []
142+
}
143+
```
144+
145+
The `pointer` stays canonical to the logical document,
146+
while `source` tells you which file the node actually lives in.

docs/@v2/v2.sidebars.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
page: commands/login.md
2727
- label: logout
2828
page: commands/logout.md
29+
- label: map
30+
page: commands/map.md
2931
- label: preview
3032
page: commands/preview.md
3133
- label: push
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Deterministic API map generation without bundling
2+
3+
The `map` command produces a tree index that tools — including LLM-based agents — can navigate to retrieve relevant sections of an API description.
4+
It is named `map` (an API map, like a sitemap for agents) rather than `toc`:
5+
the artifact is a navigable index, and "table of contents" is book vocabulary that doesn't extrapolate to AsyncAPI or Arazzo.
6+
API descriptions already carry explicit structure and native `summary`/`description` fields,
7+
so map generation is fully deterministic: no LLM calls, no network, testable with snapshots.
8+
9+
We walk the ORIGINAL resolved document (lint-style: `resolveDocument` + `walkDocument`), not the bundled one,
10+
even though `stats` bundles first.
11+
The bundler keeps no per-node map from bundled locations back to source files,
12+
while the walker's resolved locations give original `{file, pointer}` per node for free (exposed via `--source-locations`),
13+
and the canonical pointer is reconstructed from the walk key path.
14+
This also avoids bundle-time component renaming.
15+
16+
The canonical JSON pointer IS the node id — no synthetic sequential ids,
17+
because pointers are stable across regenerations and double as the retrieval address.
18+
A node's `kind` is the internal node type name the walker reports (`PathItem`, `Operation`, `Channel`) —
19+
the type system is the single source of node names, per spec.
20+
21+
## Consequences
22+
23+
- The map's pointers address the logical document as authored; `bundle` output may store `$ref`ed components under different keys.
24+
- Adding a spec means adding a visitor to the spec-version switch in `packages/core/src/api-map/build-api-map.ts`, not redesigning.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
BaseResolver,
3+
buildApiMap,
4+
detectSpec,
5+
getMajorSpecVersion,
6+
type Document,
7+
type OutputFormat,
8+
} from '@redocly/openapi-core';
9+
10+
import type { VerifyConfigOptions } from '../../types.js';
11+
import { exitWithError } from '../../utils/error.js';
12+
import { getFallbackApisOrExit } from '../../utils/miscellaneous.js';
13+
import type { CommandArgs } from '../../wrapper.js';
14+
import { printApiMap } from './print-map/index.js';
15+
16+
const SUPPORTED_MAJOR_VERSIONS = ['oas2', 'oas3', 'async2', 'async3'];
17+
18+
export type MapArgv = {
19+
api?: string;
20+
format: OutputFormat;
21+
'source-locations'?: boolean;
22+
} & VerifyConfigOptions;
23+
24+
export async function handleMap({ argv, config, collectSpecData }: CommandArgs<MapArgv>) {
25+
const [{ path }] = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
26+
const externalRefResolver = new BaseResolver(config.resolve);
27+
// resolveDocument returns a Document for a readable, parseable file; mirrors lint()
28+
const document = (await externalRefResolver.resolveDocument(null, path, true)) as Document;
29+
collectSpecData?.(document.parsed);
30+
31+
let major;
32+
try {
33+
major = getMajorSpecVersion(detectSpec(document.parsed));
34+
} catch {
35+
major = undefined;
36+
}
37+
if (!major || !SUPPORTED_MAJOR_VERSIONS.includes(major)) {
38+
exitWithError(
39+
'The `map` command supports OpenAPI and AsyncAPI descriptions only. Please provide an OpenAPI 2.0-3.2 or AsyncAPI 2.x-3.0 document.'
40+
);
41+
}
42+
43+
const apiMap = await buildApiMap({
44+
document,
45+
config,
46+
externalRefResolver,
47+
sourceLocations: argv['source-locations'],
48+
});
49+
50+
printApiMap(apiMap, path, argv.format);
51+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { logger, type ApiMapNode } from '@redocly/openapi-core';
2+
import * as colors from 'colorette';
3+
4+
import { printApiMapJson } from './json.js';
5+
import { printApiMapStylish } from './stylish.js';
6+
7+
export function printApiMap(apiMap: ApiMapNode, api: string, format: string) {
8+
switch (format) {
9+
case 'json':
10+
printApiMapJson(apiMap);
11+
break;
12+
default:
13+
logger.info(`Document: ${colors.magenta(api)} map:\n\n`);
14+
printApiMapStylish(apiMap);
15+
}
16+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { logger, type ApiMapNode } from '@redocly/openapi-core';
2+
3+
export function printApiMapJson(apiMap: ApiMapNode) {
4+
logger.output(JSON.stringify(apiMap, null, 2));
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { logger, type ApiMapNode } from '@redocly/openapi-core';
2+
import * as colors from 'colorette';
3+
4+
export function printApiMapStylish(node: ApiMapNode, depth = 0) {
5+
const indent = ' '.repeat(depth);
6+
const source = node.source ? colors.dim(` (${node.source.file}${node.source.pointer})`) : '';
7+
logger.output(
8+
`${indent}${node.title} ${colors.dim(node.kind)} ${colors.cyan(node.pointer)}${source}\n`
9+
);
10+
for (const child of node.nodes) {
11+
printApiMapStylish(child, depth + 1);
12+
}
13+
}

packages/cli/src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from './commands/generate-arazzo.js';
2323
import { handleJoin } from './commands/join/index.js';
2424
import { handleLint } from './commands/lint.js';
25+
import { handleMap } from './commands/map/index.js';
2526
import { PRODUCT_PLANS } from './commands/preview-project/constants.js';
2627
import { previewProject } from './commands/preview-project/index.js';
2728
import { handleRespect, type RespectArgv } from './commands/respect/index.js';
@@ -78,6 +79,35 @@ yargs(hideBin(process.argv))
7879
commandWrapper(handleStats)(argv);
7980
}
8081
)
82+
.command(
83+
'map [api]',
84+
'Generate a structural map of an API description.',
85+
(yargs) =>
86+
yargs
87+
.env('REDOCLY_CLI_MAP')
88+
.positional('api', { type: 'string' })
89+
.option({
90+
config: { description: 'Path to the config file.', type: 'string' },
91+
'lint-config': {
92+
description: 'Severity level for config file linting.',
93+
choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>,
94+
default: 'warn' as RuleSeverity,
95+
},
96+
format: {
97+
description: 'Use a specific output format.',
98+
choices: ['stylish', 'json'] as ReadonlyArray<OutputFormat>,
99+
default: 'stylish' as OutputFormat,
100+
},
101+
'source-locations': {
102+
description: 'Include the original source file and pointer for each node.',
103+
type: 'boolean',
104+
default: false,
105+
},
106+
}),
107+
(argv) => {
108+
commandWrapper(handleMap)(argv);
109+
}
110+
)
81111
.command(
82112
'score [api]',
83113
'Score an API description for integration simplicity and agent readiness.',

0 commit comments

Comments
 (0)