Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
721d581
docs: add graph command design spec
kanoru3101 Jun 11, 2026
0d92261
docs: add graph command implementation plan
kanoru3101 Jun 11, 2026
64d9632
feat: add dependency graph builder for graph command
kanoru3101 Jun 11, 2026
5e061ab
docs: sync graph plan with review fixes (slash, ResolveError, typeche…
kanoru3101 Jun 11, 2026
a9a894e
feat: add affected-files filter for graph command
kanoru3101 Jun 11, 2026
d5edfc5
feat: add graph command output renderers
kanoru3101 Jun 11, 2026
09d4e63
docs: reconcile mermaid label escaping in graph spec with implementation
kanoru3101 Jun 11, 2026
94b72e4
feat: register graph command in CLI
kanoru3101 Jun 11, 2026
3acea63
docs: document repeated --affected-by flag syntax in graph spec and plan
kanoru3101 Jun 11, 2026
a725935
test: add graph command e2e tests
kanoru3101 Jun 11, 2026
3b65a51
docs: document graph command and add changeset
kanoru3101 Jun 11, 2026
4afc761
docs: align spec warning wording with implementation
kanoru3101 Jun 11, 2026
b02b2f2
docs: add tree command rework spec and plan
kanoru3101 Jun 12, 2026
5cbdc48
refactor: rename graph command to tree
kanoru3101 Jun 12, 2026
643906d
feat: add pointer-to-node mapping for the tree structure view
kanoru3101 Jun 12, 2026
0fd392e
feat: add internal-structure builder for the tree command
kanoru3101 Jun 12, 2026
6c9072f
refactor: make stylish summary and empty message caller-provided
kanoru3101 Jun 12, 2026
9473d77
feat: match affected-by inputs against tree nodes
kanoru3101 Jun 12, 2026
f9a14d5
feat: make document structure the default tree view behind --files fa…
kanoru3101 Jun 12, 2026
d6e2caa
test: cover tree structure mode end to end
kanoru3101 Jun 12, 2026
8c77337
docs: document the tree command and update the changeset
kanoru3101 Jun 12, 2026
649e927
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 12, 2026
5730de6
fix: hoist entity test imports to avoid per-test transform timeout
kanoru3101 Jun 12, 2026
5eec56f
docs: unbreak documentation tests for the tree command
kanoru3101 Jun 15, 2026
da60c95
refactor(tree): share toNodeId/byString/OPERATION_METHODS via node-id
kanoru3101 Jun 15, 2026
c9fe0d7
fix: improvments
kanoru3101 Jun 15, 2026
93b9830
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 17, 2026
49904e3
refactor: tidy tree renderer sort and trim a restating comment
kanoru3101 Jun 17, 2026
a25a166
test: drop redundant tree tests (runtime round-trip, path.resolve var…
kanoru3101 Jun 17, 2026
d63acb4
docs: clarify --affected-by per mode and JSON field scope in tree docs
kanoru3101 Jun 17, 2026
d9eb1f4
test: cover multi-API rejection in default view and multi-API --files…
kanoru3101 Jun 17, 2026
2bfb339
fix: tidy tree command — share mode context, dedupe location mapping,…
kanoru3101 Jun 17, 2026
0fe4623
Merge remote-tracking branch 'origin/main' into feat/graph-command
kanoru3101 Jun 17, 2026
619ada1
fix: remove comments
kanoru3101 Jun 17, 2026
0a49e51
Merge remote-tracking branch 'origin/main' into feat/graph-command
kanoru3101 Jun 22, 2026
a9845ec
fix: refactoring
kanoru3101 Jun 22, 2026
2748189
fix: resolve bug with splitted files
kanoru3101 Jun 22, 2026
d45ab50
fix: update tests
kanoru3101 Jun 22, 2026
9afbdc0
Apply suggestions from code review
JLekawa Jun 23, 2026
c3c29af
Update tree.md
JLekawa Jun 23, 2026
21ccf18
Update docs/@v2/commands/tree.md
JLekawa Jun 23, 2026
808714d
docs(cli): fix issues in file
JLekawa Jun 23, 2026
cafbab8
Apply suggestion from @JLekawa
JLekawa Jun 23, 2026
cc10b6a
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 23, 2026
1ec983f
fix: add spaces in the options-table separator
kanoru3101 Jun 23, 2026
eeede48
fix: refactoring and update snapshots
kanoru3101 Jun 24, 2026
b2df52d
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 24, 2026
ed18a16
fix: update docs
kanoru3101 Jun 24, 2026
a3a9227
Merge branches 'feat/graph-command' and 'feat/graph-command' of githu…
kanoru3101 Jun 24, 2026
7df48d3
fix: resolve cursor bugs
kanoru3101 Jun 24, 2026
7565984
fix: update docs
kanoru3101 Jun 24, 2026
518e767
Merge branch 'main' into feat/graph-command
kanoru3101 Jun 25, 2026
ebbcc71
chore: small fixes
kanoru3101 Jun 26, 2026
16fbb5b
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 1, 2026
e75f4bb
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 2, 2026
314e9dc
feat: add level flag
kanoru3101 Jul 13, 2026
879a918
test: add snapshot
kanoru3101 Jul 13, 2026
7031128
Merge branch 'main' into feat/graph-command
kanoru3101 Jul 13, 2026
8e15c82
feat(tree): show operationId on operation nodes
kanoru3101 Jul 16, 2026
890b336
docs(tree): fix options table having a stray fourth column
kanoru3101 Jul 17, 2026
b72d3d2
feat(core): add api-graph node model and node-id mapping
kanoru3101 Aug 1, 2026
79be267
feat(core): add api-graph builder walking the original document
kanoru3101 Aug 1, 2026
67dabde
refactor(cli): back the tree structure view with the core api-graph
kanoru3101 Aug 1, 2026
849d289
test(cli): update split-fixture tree snapshots for real source files
kanoru3101 Aug 1, 2026
badc7c6
fix(core): attribute foreign-file ref edges to their spine operation
kanoru3101 Aug 1, 2026
369815e
docs(cli): document unbundled tree structure view
kanoru3101 Aug 1, 2026
2fb842c
docs(cli): fix remaining bundling-era statements in tree reference
kanoru3101 Aug 1, 2026
9a708c5
fix(cli): align uses docs, e2e titles, and callback-ref test with unb…
kanoru3101 Aug 1, 2026
442dfd7
feat(core): collect index metadata in the api-graph walk
kanoru3101 Aug 1, 2026
c0adb41
feat(core): assemble the hierarchical api index from walk metadata
kanoru3101 Aug 1, 2026
0153e3c
refactor(core): inline single-use docDescription spread helper
kanoru3101 Aug 1, 2026
8b8d43c
feat(core): add api-graph retrieval slice with envelopes and refs
kanoru3101 Aug 1, 2026
9dfdd20
fix(core): share path normalization between index and retrieval slice
kanoru3101 Aug 1, 2026
9b0beda
feat(core): add dependency closure to api-graph envelopes
kanoru3101 Aug 1, 2026
6b3e051
fix(core): gate deps-closure seeds and restrict file aliasing to comp…
kanoru3101 Aug 1, 2026
f41c4da
feat(cli): render the agent index for tree --format json
kanoru3101 Aug 1, 2026
fdf18e4
refactor(core): consolidate component-section list into one export
kanoru3101 Aug 1, 2026
842b842
fix(cli): keep split components and warn about webhooks in uses-filte…
kanoru3101 Aug 3, 2026
d1d321c
feat(cli): add --node retrieval and --with-deps closure to tree
kanoru3101 Aug 3, 2026
c1e680a
fix(cli): match components by file only when split from the root docu…
kanoru3101 Aug 3, 2026
39fd095
fix(cli): apply --level as-is to --node sub-indexes and add the chang…
kanoru3101 Aug 3, 2026
4beb2d9
refactor: consolidate tree changesets, drop interim wrappers, documen…
kanoru3101 Aug 3, 2026
74e4b8e
docs(cli): complete tree usage and options for the agent index flags
kanoru3101 Aug 3, 2026
072800d
Merge remote-tracking branch 'origin/main' into feat/tree-v2
kanoru3101 Aug 3, 2026
26a35f3
feat(core): keep canonical component ids for split whole-file refs
kanoru3101 Aug 3, 2026
f5257d1
docs: add a guide measuring agent context savings from the tree index
kanoru3101 Aug 3, 2026
b616f1c
docs: state that the JSON index requires an OpenAPI description
kanoru3101 Aug 3, 2026
e248d68
docs: add Google Compute and GitHub scaling cases to the tree index b…
kanoru3101 Aug 4, 2026
db69271
docs: list the tree benchmark guide on the guides index
kanoru3101 Aug 4, 2026
55bd506
fix(core): take the tree index Servers section from the root server list
kanoru3101 Aug 4, 2026
ad2690a
docs: rebuild the tree index benchmark on the GitHub REST API descrip…
kanoru3101 Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/tree-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@redocly/openapi-core': minor
'@redocly/cli': minor
---

Added the experimental `tree` command: it prints the structure of an API description — paths, operations, and the `$ref` dependency chains between them — with every node attributed to the file that defines it, and runs impact analysis with `--uses` (which paths and operations use a given component or file).
For LLM agents and tooling, `--format=json` prints a hierarchical index with stable semantic ids, JSON pointers, source files, line ranges, and summaries taken from the description itself; `--node` returns one node (a branch as a sub-index, a leaf as its raw source lines with resolved `$ref`s), and `--with-deps` appends the node's transitive `$ref` closure.
The underlying engines live in `@redocly/openapi-core`'s new `api-graph` module (`analyzeApi`, `buildApiIndex`, `buildNodeEnvelope`, `appendDepsClosure`).
1 change: 1 addition & 0 deletions docs/@v2/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ API management commands:
- [`score`](score.md) Score an API for integration simplicity and AI agent readiness.
- [`split`](split.md) Split API description into a multi-file structure.
- [`stats`](stats.md) Gather statistics for a document.
- [`tree`](tree.md) Display the structure of an API description as a tree.

Linting commands:

Expand Down
583 changes: 583 additions & 0 deletions docs/@v2/commands/tree.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/@v2/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ Authenticate, handle errors, and compose middleware with a client from `generate
Pre-configure publisher defaults and write custom client generators.
{% /card %}

{% card title="Agent context savings with tree"
to="./tree-agent-index-benchmark"
%}
Measured token counts for exploring GitHub's 9.8 MB REST API description with the `tree` index instead of reading the whole file.
{% /card %}

{% card title="Set up tab completion"
to="./autocomplete"
%}
Expand Down
215 changes: 215 additions & 0 deletions docs/@v2/guides/tree-agent-index-benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# How much context the `tree` index saves an agent

The [`tree`](../commands/tree.md) command's JSON index lets an AI agent work with an API description that does not fit in its context window.
This guide measures that on the largest well-known public API description: GitHub's official REST API description, 9.8 MB of OpenAPI.
For the command reference, see [`tree`](../commands/tree.md).

Every number below comes from a real command run against that file, tokenized with a BPE tokenizer (`gpt-tokenizer`, o200k family; other model families tokenize slightly differently, with the same order of magnitude).
The description is public, so the whole experiment is reproducible:

```bash
curl -O https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml
```

## The setup

- **Description:** `api.github.com.yaml` from [`github/rest-api-description`](https://github.com/github/rest-api-description) — 9.8 MB, OpenAPI 3.0.3, 47 tags, 1,216 operations, 1,766 components.
This is the first-party description GitHub's own SDKs are generated from, not a conversion or a sample.
- **Agent task:** _"Create a repository for the authenticated user."_
- **Agent constraints:** a 200,000-token context window; the agent starts knowing nothing about the description.
- **What the agent is told up front:** a short instruction naming the three commands (index → branch → leaf-with-deps) and the id forms — **114 tokens**, measured.
The agent decides _which_ branch and operation to open by reasoning over titles and summaries; it does not discover the commands themselves.
That one-time cost is about 0.2% of the chain below and appears as a separate line in the totals.

## Without the index

The agent's only option is to read the description:

| Input | Tokens |
| --------------------------------- | ------------: |
| `api.github.com.yaml`, whole file | **1,946,991** |

At 1,946,991 tokens the file is roughly ten times a 200,000-token window, and still twice a 1,000,000-token one.
No amount of "read a bit more" helps here.
Searching the file by text instead is unreliable: it does not reveal the structure, does not follow `$ref` chains, and gives no bound on how much context the agent ends up reading.

## Why the index has to be hierarchical

At this size, a flat index does not solve the problem either:

| Input | Tokens | Nodes |
| ------------------------------------------------ | ----------: | ----: |
| `redocly tree api.github.com.yaml --format=json` | **306,494** | 3,038 |

The complete index of every tag, operation, and component is itself larger than the context window.
This is what the `--level` and `--node` options are for: the agent never asks for the whole index, only for one level or one branch at a time.
On this description the hierarchy is not an optimization — it is the only way an agent can work with the file at all.

## With the index

The agent walks the hierarchy in bounded steps, paying only for the path it chooses:

| Step | Command | Output size | Tokens |
| ------------------------------------------------ | ------------------------------------------------------------------------ | ----------: | ---------: |
| 1. Map the spec — 4 sections, 47 tags | `redocly tree api.github.com.yaml --format=json --level 2` | 14.6 KB | 3,647 |
| 2. Open the branch it picked — 203 operations | `redocly tree api.github.com.yaml --node repos` | 101.0 KB | 27,017 |
| 3. Fetch the target with its full `$ref` closure | `redocly tree api.github.com.yaml --node 'POST /user/repos' --with-deps` | 80.4 KB | 18,946 |
| **Total** | | | **49,610** |

Step 3 returns a _self-contained_ slice: the operation's raw source lines (8.3 KB) plus the 14 components it transitively references — the `full-repository` schema and everything under it, the seven shared error responses, the response example — in dependency order.
That fills 63.6 KB of the 64 KB closure cap, so the response stays bounded no matter how deep the schema graph goes; anything beyond the cap stays one `--node` call away.

The most expensive step is not the largest file, it is the largest branch: `repos` is GitHub's biggest tag, and listing its 203 operations costs more than the operation and all its schemas combined.
An agent that already knows the tag can start from `--level 1` (286 tokens) and skip straight to it.

## What the agent actually sees

Step 1 is small enough to show in full — this is the entire map of a 9.8 MB API in 286 tokens:

```json
{
"docName": "api.github.com.yaml",
"spec": "oas3_0",
"docDescription": "GitHub v3 REST API — GitHub's v3 REST API.",
"structure": [
{
"id": "Overview",
"title": "Overview",
"pointer": "#/info",
"file": "api.github.com.yaml",
"start_line": 4,
"end_line": 14,
"summary": "GitHub's v3 REST API."
},
{
"id": "Servers",
"title": "Servers",
"pointer": "#/servers",
"file": "api.github.com.yaml",
"start_line": 116,
"end_line": 116,
"summary": "https://api.github.com"
},
{
"id": "Operations",
"title": "Operations",
"pointer": "#/paths",
"file": "api.github.com.yaml",
"start_line": 121,
"end_line": 67148
},
{
"id": "Components",
"title": "Components",
"pointer": "#/components",
"file": "api.github.com.yaml",
"start_line": 85076,
"end_line": 261104
}
]
}
```

Step 2 opens one branch and returns its operations, each with the summary the agent reasons over and the exact lines it can read directly:

```json
{
"structure": [
{
"id": "repos",
"title": "repos",
"pointer": "#/tags/25",
"file": "api.github.com.yaml",
"start_line": 66,
"end_line": 67,
"summary": "Interact with GitHub Repos.",
"nodes": [
{
"id": "POST /user/repos",
"title": "POST /user/repos — Create a repository for the authenticated user",
"operationId": "repos/create-for-authenticated-user",
"pointer": "#/paths/~1user~1repos/post",
"file": "api.github.com.yaml",
"start_line": 62491,
"end_line": 62697,
"summary": "Create a repository for the authenticated user"
}
]
}
]
}
```

Step 3 returns the leaf envelope: raw source lines, the `$ref`s found inside them resolved to real locations, and the transitive closure under `deps`:

```json
{
"id": "POST /user/repos",
"pointer": "#/paths/~1user~1repos/post",
"file": "api.github.com.yaml",
"start_line": 62491,
"end_line": 62697,
"content": "summary: Create a repository for the authenticated user\ndescription: Creates a new repository for the authenticated user.\ntags:\n - repos\noperationId: repos/create-for-authenticated-user\n…",
"refs": [
{
"ref": "#/components/responses/bad_request",
"resolved": true,
"file": "api.github.com.yaml",
"pointer": "#/components/responses/bad_request"
}
],
"deps": [
{ "id": "schemas/full-repository", "file": "api.github.com.yaml", "content": "…" },
{ "id": "schemas/nullable-repository", "file": "api.github.com.yaml", "content": "…" },
{ "id": "responses/validation_failed", "file": "api.github.com.yaml", "content": "…" }
]
}
```

The 14 ids returned in the closure: `schemas/full-repository`, `schemas/nullable-repository`, `schemas/nullable-license-simple`, `schemas/code-of-conduct-simple`, `schemas/basic-error`, `schemas/scim-error`, `schemas/validation-error`, `examples/full-repository`, and the `responses/*` entries for the seven documented error codes.

## The same task on a split (multi-file) layout

The same description was run through [`redocly split`](../commands/split.md), producing **2,842 files**, and the identical chain was repeated against `openapi.yaml` in that directory:

| Step | Single file | Split (2,842 files) |
| ------------------------------------------ | ----------: | ------------------: |
| 1. `--format=json --level 2` | 3,647 | 3,436 |
| 2. `--node repos` | 27,017 | 23,709 |
| 3. `--node 'POST /user/repos' --with-deps` | 18,946 | 18,807 |
| **Chain total** | **49,610** | **45,952** |

The split chain is slightly cheaper, because pointers inside small files are short.
Both layouts list the same 203 operations under `repos`, and operation ids are identical (`POST /user/repos`), so the same agent instructions work unchanged.

Component ids differ between the layouts, and it is worth knowing why.
In the single file, components are declared under `components`, so they get canonical ids: `schemas/full-repository`.
`redocly split` does not keep a component registry in the root document — operation files reference component files directly — so in that layout the same schema is identified by its path: `components/schemas/full-repository.yaml`.
Canonical ids appear in a split layout too, as long as the root document declares the component (`components: {schemas: {Name: {$ref: ./file.yaml}}}`), which is what a hand-maintained multi-file description usually does.
Either way the closure is retrieved by one command: here it pulled 15 components from 15 separate files and returned them as a single envelope — the case where an agent without an index would have to hand-walk `$ref`s across a 2,842-file tree without knowing which ones matter.

## The difference

| | Tokens | vs. whole file |
| ---------------------------------- | ------------------------: | ---------------: |
| Whole file | 1,946,991 | — (does not fit) |
| Full index, unfiltered | 306,494 | — (does not fit) |
| Index chain | 49,610 (+114 instruction) | **~39× less** |
| Index chain, starting from level 1 | 46,249 (+114 instruction) | **~42× less** |
| Index chain on the split layout | 45,952 (+114 instruction) | **~42× less** |

The ratio matters less than the shape of the curve.
The chain's cost is bounded by the _largest branch_ and the _deepest single closure_, not by the size of the description: on a 1.3 MB description the same three steps cost 12,000 to 25,000 tokens, and on this 9.8 MB one they cost about 50,000.
The description grew by a factor of 7.5; the chain roughly doubled.

For descriptions that fit the context window, the index saves tokens.
Past the window size, it is the difference between an impossible task and a routine one — here the agent solves a task against a two-million-token API while using a quarter of a 200,000-token window, with the rest left for the work itself.

## Methodology notes

- Every output above comes from a real command run against the real file; sizes are the byte counts of captured `stdout`.
- Token counts come from `gpt-tokenizer` over the exact captured text, not from a characters-per-token estimate.
- The JSON samples are real command output, shortened by dropping whole nodes and eliding long string values with `…`, never by rewriting values; the file name is shortened from the local path to `api.github.com.yaml`.
- The description is `api.github.com.yaml` from the `main` branch of `github/rest-api-description`, version 1.1.4, used unmodified.
- The agent chooses which nodes to open; the command syntax comes from the 114-token instruction counted separately above.
- Each command invocation analyzes the description again — about 42 seconds for this 9.8 MB file. A long-running process that keeps the analysis in memory would pay that cost once per session instead of once per step.
4 changes: 4 additions & 0 deletions docs/@v2/v2.sidebars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
page: commands/stats.md
- label: translate
page: commands/translate.md
- label: tree
page: commands/tree.md
- group: Guides
page: guides/index.md
items:
Expand All @@ -66,6 +68,8 @@
page: guides/use-generated-client.md
- label: Customize client generation
page: guides/customize-client-generation.md
- label: Agent context savings with tree
page: guides/tree-agent-index-benchmark.md
- label: Hide internal APIs
page: guides/hide-apis.md
- label: Replace the servers URL
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ export async function handleLintConfig(argv: Exact<CommandArgv>, version: string
return;
}

if (argv.format === 'json' || argv.format === 'junit' || argv.format === 'checkstyle') {
if (
argv.format === 'json' ||
argv.format === 'junit' ||
argv.format === 'checkstyle' ||
argv.format === 'mermaid' ||
argv.format === 'dot'
) {
// these are single-document formats, so a separate config-lint document would break the output
return;
}
Expand Down
Loading
Loading