Skip to content

feat(api): add api spec <spec> command#1073

Draft
gu-stav wants to merge 10 commits into
feat/api-command-listfrom
feat/api-command-spec-cmd
Draft

feat(api): add api spec <spec> command#1073
gu-stav wants to merge 10 commits into
feat/api-command-listfrom
feat/api-command-spec-cmd

Conversation

@gu-stav
Copy link
Copy Markdown
Member

@gu-stav gu-stav commented May 13, 2026

Description

Adds sanity api spec <slug> for inspecting one OpenAPI spec.

  • Default: structured human view — header + per-operation blocks (method, endpoint, operationId, capability tags, summary, typed params, request body, responses, auth, "Schemas referenced" footer).
  • --format=json — per-operation JSON envelope, agent-friendly.
  • --format=openapi — raw YAML, byte-for-byte passthrough of the upstream spec.
  • --operation=<id> — narrow any output mode to a single operation.
  • --schema <name> — print one components.schemas.<name> entry (follows $ref pointers from operation output). Honors --format (default YAML).
  • --web — open the docs page in a browser.

Deprecates sanity openapi get (passthrough output preserved during the back-compat window).

This PR is the second in a stack:

Architecture refactors (post-review)

  • $ref policy: link, never resolve. Body/response schemas carry ref: '<schemaName>'; agents follow up with --schema <name>.
  • Drop OperationJsonView pass-through projection — buildSpecJsonView now uses ParsedOperation directly (~30 lines + one type gone).
  • Use shared docsUrlFor() + loadSingleSpecOrThrow() helpers from list-branch refactor; drop local HTTP_REFERENCE_BASE_URL and the bespoke loadSpec private method.

PR review items addressed

  • Reject --operation + --format=openapi upfront (typo'd op no longer silently produces full raw YAML).
  • Debug-log when extractRequestBody skips a body-level $ref (the operation would otherwise render as bodyless without trace).
  • Gate "Query params (required)" behind a length check — optional-only operations no longer render a dead (none) block.

What to review

  • packages/@sanity/cli/src/commands/api/spec.ts — the new command.
  • packages/@sanity/cli/src/api/parser.ts + extractors.ts + internal.ts — the rich OpenAPI parser introduced for this PR.
  • packages/@sanity/cli/src/api/views.ts — list view + spec view, both human and JSON.
  • packages/@sanity/cli/src/commands/openapi/get.ts — deprecation forwarder.

Testing

15 unit tests in commands/api/__tests__/spec.test.ts covering: human view, JSON envelope, OpenAPI YAML passthrough, --operation narrow + unknown-op error, --schema YAML default + JSON + unknown-schema error, --operation + --format=openapi conflict, optional-only query gating, --web flow, unknown slug, docs service unreachable. Plus 33 parser tests.

@gu-stav gu-stav requested a review from a team as a code owner May 13, 2026 08:42
@claude
Copy link
Copy Markdown

claude Bot commented May 13, 2026

Claude finished @gu-stav's task in 3m 15s —— View job


Review

The Cursor Bugbot finding (--operation silently ignored with --format=openapi) is already fixed in f1e517fspec.ts:86-92 rejects the combo upfront. Bugbot was reviewing the older commit e28984b4. No action needed.

A small set of issues worth addressing below; nothing blocking.


1. Changeset includes an internal rename that never reached users

File: .changeset/add-api-spec-command.md

The per-operation JSON renames path to openApiPath to disambiguate it from the callable endpoint.

This rename happened during PR review of an unreleased command (commit f9a9ed8). Since sanity api spec is brand new in this changeset, there is no path → openApiPath rename for users — the field is just openApiPath from day one. The sentence describes internals and inflates the entry. Also: per the product-copy guidance, the summary is long and packs multiple changes into one paragraph.

Suggested tighter copy:

Adds `sanity api spec <slug>` for inspecting one OpenAPI spec. Default
output is a structured per-operation view; `--format=json` emits a JSON
envelope, `--format=openapi` passes through the raw YAML, `--operation`
narrows to one operation, and `--schema <name>` prints one component
schema. Deprecates `sanity openapi get` (output preserved during the
back-compat window).

Fix this →


2. --format=yaml without --schema silently produces the human view

File: packages/@sanity/cli/src/commands/api/spec.ts:53-59, 144

--format=yaml is only meaningful with --schema. Without it, the code falls through to renderSpecHumanView — neither YAML nor an error. The flag description does call this out ("paired with --schema; no effect otherwise"), but this is exactly the class of silent-no-op that the other upfront guards (lines 86, 98, 109) were added to prevent. The user gets identical output to omitting the flag, with no signal that they typed something meaningless.

Suggested: reject the combo upfront, matching the other guards:

if (flags.format === 'yaml' && !flags.schema) {
  this.error(
    '`--format=yaml` only applies with `--schema`. Drop `--format` or add `--schema <name>`.',
    {exit: 1},
  )
}

Tests in spec.test.ts don't cover this case today either way.

Fix this →


3. --web silently ignores other flags

File: packages/@sanity/cli/src/commands/api/spec.ts:76-81

sanity api spec jobs --web --operation=foo opens the docs page and silently drops --operation. Same with --schema, --format=openapi, etc. The PR adds upfront guards for other silent-ignore footguns (--format=openapi + --operation, --schema + --operation) but not for --web. Low-stakes since --web is interactive, but reads inconsistent next to the other guards.

Optional: reject --web combined with any of --operation, --schema, --format. Or document the precedence explicitly in the flag description.


4. Composition flattening only runs at the body root

File: packages/@sanity/cli/src/api/extractors.ts:206-247

walkProperty / walkProperties handle nested objects but do not call flattenComposition on them. So a request body of the shape:

properties:
  payload:
    allOf:
      - { properties: { a: { type: string } } }
      - { properties: { b: { type: string } } }

renders payload with empty fields and no refs surfaced. The root-level body handles this correctly via flattenComposition, but nested composition is missed. None of Sanity's current specs may hit this, but worth a debug log or a follow-up note in the doc-comment so the gap is auditable later.


5. extractResponses drops response-level $refs entirely

File: packages/@sanity/cli/src/api/extractors.ts:386

if (asString(response.$ref)) continue

The comment explains: "uncommon in our specs, and the schema lookup covers the use case if anyone needs it." But the row is silently dropped without a debug log, unlike the body-level $ref skip (lines 326-330) which does log. Consider symmetry — add a debug(...) so the same observability applies when a spec starts using this pattern.


6. Coverage: extractors.ts at 82.7%

The new extractor module is the lowest-covered new file. From a quick scan, the uncovered branches likely include the response $ref skip (point 5 above), the body-level $ref skip path, and parts of flattenComposition's oneOf/anyOf fork. None are user-facing critical, but worth a follow-up if you want full coverage parity with parser.ts (94.8%) and spec.ts (100%).


Summary

  • Read the PR diff against base branch
  • Review commands/api/spec.ts
  • Review api/parser.ts + extractors.ts
  • Review api/views.ts
  • Review commands/openapi/get.ts deprecation
  • Check tests and changeset
  • Post final review

The architecture is solid — link-not-resolve $ref policy is well-motivated and consistently applied, the conflict guards added in f1e517f close the main silent-ignore footguns, and the parse/fetch error split in loadSingleSpecOrThrow cleanly distinguishes "spec is bad" from "service is unavailable." Items above are tightening, not redirecting.
· Branch

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

📦 Bundle Stats — @sanity/cli

Compared against feat/api-command-list (309e558a)

@sanity/cli

Metric Value vs feat/api-command-list (309e558)
Internal (raw) 2.1 KB -
Internal (gzip) 799 B -
Bundled (raw) 10.97 MB -
Bundled (gzip) 2.06 MB -
Import time 869ms +12ms, +1.4%

bin:sanity

Metric Value vs feat/api-command-list (309e558)
Internal (raw) 975 B -
Internal (gzip) 460 B -
Bundled (raw) 9.84 MB -
Bundled (gzip) 1.77 MB -
Import time 976ms +10ms, +1.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against feat/api-command-list (309e558a)

Metric Value vs feat/api-command-list (309e558)
Internal (raw) 95.5 KB -
Internal (gzip) 22.5 KB -
Bundled (raw) 21.60 MB -
Bundled (gzip) 3.42 MB -
Import time 821ms +8ms, +1.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — create-sanity

Compared against feat/api-command-list (309e558a)

Metric Value vs feat/api-command-list (309e558)
Internal (raw) 976 B -
Internal (gzip) 507 B -
Bundled (raw) 50.7 KB -
Bundled (gzip) 12.6 KB -
Import time ❌ ChildProcess denied: node -
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@gu-stav gu-stav marked this pull request as draft May 13, 2026 08:46
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e28984b. Configure here.

if (flags.format === 'openapi') {
this.log(loaded.yaml)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--operation flag silently ignored with --format=openapi

Medium Severity

The --format=openapi code path returns early at line 92–95, before selectOperations is called at line 97. This means --operation=<id> is completely silently ignored — neither applied nor validated — when combined with --format=openapi. A user passing --operation=nonexistentId --format=openapi receives the full spec YAML with no error or warning, contradicting the documented behavior and the command's own example description stating --operation "narrows any output mode to a single operation."

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e28984b. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

Coverage Delta

File Statements
packages/@sanity/cli/scripts/check-topic-aliases.ts 0.0% (±0%)
packages/@sanity/cli/src/api/docsClient.ts 96.0% (new)
packages/@sanity/cli/src/api/extractors.ts 83.5% (new)
packages/@sanity/cli/src/api/parser.ts 94.9% (new)
packages/@sanity/cli/src/api/views.ts 66.7% (new)
packages/@sanity/cli/src/commands/api/list.ts 97.3% (new)
packages/@sanity/cli/src/commands/api/spec.ts 100.0% (new)
packages/@sanity/cli/src/commands/openapi/get.ts 100.0% (±0%)
packages/@sanity/cli/src/commands/openapi/list.ts 100.0% (±0%)

Comparing 9 changed files against main @ 3e479c080419f692b9e2ff4719e30843435f04e6

Overall Coverage

Metric Coverage
Statements 84.2% (±0%)
Branches 73.8% (- 0.3%)
Functions 84.5% (+ 0.5%)
Lines 84.8% (+ 0.2%)

@gu-stav gu-stav changed the title feat: api spec command feat(api): add spec command May 13, 2026
@gu-stav gu-stav changed the title feat(api): add spec command feat(api): add api spec command May 13, 2026
gu-stav added a commit that referenced this pull request May 13, 2026
PR review fixes (#1073):
- walkProperty: surface element schema name on `ref` for `array<$ref>`
  body fields (and the equivalent on path/query params). Without this,
  the schemas-referenced footer silently omitted the array element type.
- Stale `parseOpenApi` docstring updated to match the synthesize-and-
  debug-log behavior.
- Path-item + operation-level params now dedup by `(name, in)` with
  operation-level wins (per OpenAPI 3.x).
- `isStreamingResponse` now matches any 2xx with `text/event-stream`
  (was 200-only).
- Tighten the changeset to a single user-facing sentence.

Architecture improvements (from /improve-codebase-architecture):
- Unify HTTP fetch: `docsClient.fetchSpec(slug, {format?})` now
  accepts `'yaml' | 'json'`. `openapi/get.ts` drops its hand-rolled
  fetch + URL/timeout/headers constants and goes through docsClient,
  so the docs-endpoint URL + Vercel bypass token + 404 handling
  live in one module.
- Thread component schemas through `ParsedSpec.schemas` instead of
  re-parsing YAML in `--schema <name>`. Drops `lookupComponentSchema`
  / `listComponentSchemas` exports and the `yaml` import from
  parser.ts. One YAML parse per `--schema` invocation, no second
  parse pass.
- Split parser.ts (780 lines) into three files: `parser.ts` keeps
  types + capability + URL helpers + parseOpenApi orchestration +
  loaders; `extractors.ts` holds the per-aspect extractors
  (parameters / requestBody / responses / security); `internal.ts`
  holds shared shape-coercion helpers (asObject/asString/asArray)
  and the OpenAPI-shape utilities (schemaRefName / describeType /
  summarizeInlineObject). Dependency direction: parser → extractors
  → internal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gu-stav gu-stav changed the title feat(api): add api spec command feat(api): add api spec <spec> command May 13, 2026
gu-stav added a commit that referenced this pull request May 13, 2026
PR review fixes (#1073):
- walkProperty: surface element schema name on `ref` for `array<$ref>`
  body fields (and the equivalent on path/query params). Without this,
  the schemas-referenced footer silently omitted the array element type.
- Stale `parseOpenApi` docstring updated to match the synthesize-and-
  debug-log behavior.
- Path-item + operation-level params now dedup by `(name, in)` with
  operation-level wins (per OpenAPI 3.x).
- `isStreamingResponse` now matches any 2xx with `text/event-stream`
  (was 200-only).
- Tighten the changeset to a single user-facing sentence.

Architecture improvements (from /improve-codebase-architecture):
- Unify HTTP fetch: `docsClient.fetchSpec(slug, {format?})` now
  accepts `'yaml' | 'json'`. `openapi/get.ts` drops its hand-rolled
  fetch + URL/timeout/headers constants and goes through docsClient,
  so the docs-endpoint URL + Vercel bypass token + 404 handling
  live in one module.
- Thread component schemas through `ParsedSpec.schemas` instead of
  re-parsing YAML in `--schema <name>`. Drops `lookupComponentSchema`
  / `listComponentSchemas` exports and the `yaml` import from
  parser.ts. One YAML parse per `--schema` invocation, no second
  parse pass.
- Split parser.ts (780 lines) into three files: `parser.ts` keeps
  types + capability + URL helpers + parseOpenApi orchestration +
  loaders; `extractors.ts` holds the per-aspect extractors
  (parameters / requestBody / responses / security); `internal.ts`
  holds shared shape-coercion helpers (asObject/asString/asArray)
  and the OpenAPI-shape utilities (schemaRefName / describeType /
  summarizeInlineObject). Dependency direction: parser → extractors
  → internal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gu-stav added a commit that referenced this pull request May 13, 2026
- Reject `--operation` + `--format=openapi` upfront (typo'd op
  no longer silently produces full raw YAML).
- Debug-log when extractRequestBody skips a body-level `$ref`;
  the operation otherwise renders as bodyless without trace.
- Gate "Query params (required)" behind a length check so
  optional-only operations don't render a dead `(none)` block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gu-stav gu-stav force-pushed the feat/api-command-spec-cmd branch from 5026641 to f006816 Compare May 13, 2026 11:05
gu-stav added a commit that referenced this pull request May 13, 2026
PR review fixes (#1073):
- walkProperty: surface element schema name on `ref` for `array<$ref>`
  body fields (and the equivalent on path/query params). Without this,
  the schemas-referenced footer silently omitted the array element type.
- Stale `parseOpenApi` docstring updated to match the synthesize-and-
  debug-log behavior.
- Path-item + operation-level params now dedup by `(name, in)` with
  operation-level wins (per OpenAPI 3.x).
- `isStreamingResponse` now matches any 2xx with `text/event-stream`
  (was 200-only).
- Tighten the changeset to a single user-facing sentence.

Architecture improvements (from /improve-codebase-architecture):
- Unify HTTP fetch: `docsClient.fetchSpec(slug, {format?})` now
  accepts `'yaml' | 'json'`. `openapi/get.ts` drops its hand-rolled
  fetch + URL/timeout/headers constants and goes through docsClient,
  so the docs-endpoint URL + Vercel bypass token + 404 handling
  live in one module.
- Thread component schemas through `ParsedSpec.schemas` instead of
  re-parsing YAML in `--schema <name>`. Drops `lookupComponentSchema`
  / `listComponentSchemas` exports and the `yaml` import from
  parser.ts. One YAML parse per `--schema` invocation, no second
  parse pass.
- Split parser.ts (780 lines) into three files: `parser.ts` keeps
  types + capability + URL helpers + parseOpenApi orchestration +
  loaders; `extractors.ts` holds the per-aspect extractors
  (parameters / requestBody / responses / security); `internal.ts`
  holds shared shape-coercion helpers (asObject/asString/asArray)
  and the OpenAPI-shape utilities (schemaRefName / describeType /
  summarizeInlineObject). Dependency direction: parser → extractors
  → internal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gu-stav added a commit that referenced this pull request May 13, 2026
- Reject `--operation` + `--format=openapi` upfront (typo'd op
  no longer silently produces full raw YAML).
- Debug-log when extractRequestBody skips a body-level `$ref`;
  the operation otherwise renders as bodyless without trace.
- Gate "Query params (required)" behind a length check so
  optional-only operations don't render a dead `(none)` block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gu-stav gu-stav force-pushed the feat/api-command-spec-cmd branch from f006816 to f13f93f Compare May 13, 2026 12:58
gu-stav added a commit that referenced this pull request May 13, 2026
PR review fixes (#1073):
- walkProperty: surface element schema name on `ref` for `array<$ref>`
  body fields (and the equivalent on path/query params). Without this,
  the schemas-referenced footer silently omitted the array element type.
- Stale `parseOpenApi` docstring updated to match the synthesize-and-
  debug-log behavior.
- Path-item + operation-level params now dedup by `(name, in)` with
  operation-level wins (per OpenAPI 3.x).
- `isStreamingResponse` now matches any 2xx with `text/event-stream`
  (was 200-only).
- Tighten the changeset to a single user-facing sentence.

Architecture improvements (from /improve-codebase-architecture):
- Unify HTTP fetch: `docsClient.fetchSpec(slug, {format?})` now
  accepts `'yaml' | 'json'`. `openapi/get.ts` drops its hand-rolled
  fetch + URL/timeout/headers constants and goes through docsClient,
  so the docs-endpoint URL + Vercel bypass token + 404 handling
  live in one module.
- Thread component schemas through `ParsedSpec.schemas` instead of
  re-parsing YAML in `--schema <name>`. Drops `lookupComponentSchema`
  / `listComponentSchemas` exports and the `yaml` import from
  parser.ts. One YAML parse per `--schema` invocation, no second
  parse pass.
- Split parser.ts (780 lines) into three files: `parser.ts` keeps
  types + capability + URL helpers + parseOpenApi orchestration +
  loaders; `extractors.ts` holds the per-aspect extractors
  (parameters / requestBody / responses / security); `internal.ts`
  holds shared shape-coercion helpers (asObject/asString/asArray)
  and the OpenAPI-shape utilities (schemaRefName / describeType /
  summarizeInlineObject). Dependency direction: parser → extractors
  → internal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gu-stav added a commit that referenced this pull request May 13, 2026
- Reject `--operation` + `--format=openapi` upfront (typo'd op
  no longer silently produces full raw YAML).
- Debug-log when extractRequestBody skips a body-level `$ref`;
  the operation otherwise renders as bodyless without trace.
- Gate "Query params (required)" behind a length check so
  optional-only operations don't render a dead `(none)` block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gu-stav gu-stav force-pushed the feat/api-command-spec-cmd branch from f13f93f to d10a918 Compare May 13, 2026 13:43
gu-stav added a commit that referenced this pull request May 13, 2026
PR review fixes (#1073):
- walkProperty: surface element schema name on `ref` for `array<$ref>`
  body fields (and the equivalent on path/query params). Without this,
  the schemas-referenced footer silently omitted the array element type.
- Stale `parseOpenApi` docstring updated to match the synthesize-and-
  debug-log behavior.
- Path-item + operation-level params now dedup by `(name, in)` with
  operation-level wins (per OpenAPI 3.x).
- `isStreamingResponse` now matches any 2xx with `text/event-stream`
  (was 200-only).
- Tighten the changeset to a single user-facing sentence.

Architecture improvements (from /improve-codebase-architecture):
- Unify HTTP fetch: `docsClient.fetchSpec(slug, {format?})` now
  accepts `'yaml' | 'json'`. `openapi/get.ts` drops its hand-rolled
  fetch + URL/timeout/headers constants and goes through docsClient,
  so the docs-endpoint URL + Vercel bypass token + 404 handling
  live in one module.
- Thread component schemas through `ParsedSpec.schemas` instead of
  re-parsing YAML in `--schema <name>`. Drops `lookupComponentSchema`
  / `listComponentSchemas` exports and the `yaml` import from
  parser.ts. One YAML parse per `--schema` invocation, no second
  parse pass.
- Split parser.ts (780 lines) into three files: `parser.ts` keeps
  types + capability + URL helpers + parseOpenApi orchestration +
  loaders; `extractors.ts` holds the per-aspect extractors
  (parameters / requestBody / responses / security); `internal.ts`
  holds shared shape-coercion helpers (asObject/asString/asArray)
  and the OpenAPI-shape utilities (schemaRefName / describeType /
  summarizeInlineObject). Dependency direction: parser → extractors
  → internal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gu-stav added a commit that referenced this pull request May 13, 2026
- Reject `--operation` + `--format=openapi` upfront (typo'd op
  no longer silently produces full raw YAML).
- Debug-log when extractRequestBody skips a body-level `$ref`;
  the operation otherwise renders as bodyless without trace.
- Gate "Query params (required)" behind a length check so
  optional-only operations don't render a dead `(none)` block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gu-stav gu-stav force-pushed the feat/api-command-spec-cmd branch from d10a918 to 6bf31f5 Compare May 13, 2026 14:30
gu-stav and others added 6 commits May 13, 2026 17:16
Adds `sanity api spec <slug>` for inspecting a single public Sanity
HTTP spec. Three output modes via a single `--format` flag:

  - default (no flag) → structured human view (header + per-operation
    blocks with typed params, request body, responses, auth, plus a
    `Schemas referenced` footer pointing at `--schema`).
  - `--format=json`   → structured per-operation JSON.
  - `--format=openapi`→ raw OpenAPI YAML (byte-for-byte passthrough).

`--operation=<id>` narrows any of the three modes to a single
operation. `--schema=<name>` prints one `components.schemas` entry
(YAML default, JSON with `--format=json`) — the follow-up for `$ref`
pointers surfaced in operation output.

`sanity openapi get` is converted to a deprecation forwarder that
preserves its pre-deprecation passthrough output (raw OpenAPI body,
YAML default, JSON with `--format=json`) so scripts piping stdout
keep working. New structured shape lives on `sanity api spec`.

The parser is enriched to back the spec view: typed parameter objects
(name/in/type/required/description/default/enum/example/ref),
structured request body with composition flattening
(allOf/oneOf/anyOf), responses with schema summaries, security with
case-normalized scheme names. `$ref` policy: link, don't resolve —
schema refs surface as `ref: "<name>"`. Parameter refs DO resolve
inline (path params must survive). `list --json` projects names from
the new param objects so its shape is unchanged.

Operations missing `operationId` get a deterministic synthesized id
(`<method>_<path>`) instead of being skipped. Several Sanity specs
(e.g. `mutation`) omit it; the synthesis keeps every row addressable
without losing visibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR review fixes (#1073):
- walkProperty: surface element schema name on `ref` for `array<$ref>`
  body fields (and the equivalent on path/query params). Without this,
  the schemas-referenced footer silently omitted the array element type.
- Stale `parseOpenApi` docstring updated to match the synthesize-and-
  debug-log behavior.
- Path-item + operation-level params now dedup by `(name, in)` with
  operation-level wins (per OpenAPI 3.x).
- `isStreamingResponse` now matches any 2xx with `text/event-stream`
  (was 200-only).
- Tighten the changeset to a single user-facing sentence.

Architecture improvements (from /improve-codebase-architecture):
- Unify HTTP fetch: `docsClient.fetchSpec(slug, {format?})` now
  accepts `'yaml' | 'json'`. `openapi/get.ts` drops its hand-rolled
  fetch + URL/timeout/headers constants and goes through docsClient,
  so the docs-endpoint URL + Vercel bypass token + 404 handling
  live in one module.
- Thread component schemas through `ParsedSpec.schemas` instead of
  re-parsing YAML in `--schema <name>`. Drops `lookupComponentSchema`
  / `listComponentSchemas` exports and the `yaml` import from
  parser.ts. One YAML parse per `--schema` invocation, no second
  parse pass.
- Split parser.ts (780 lines) into three files: `parser.ts` keeps
  types + capability + URL helpers + parseOpenApi orchestration +
  loaders; `extractors.ts` holds the per-aspect extractors
  (parameters / requestBody / responses / security); `internal.ts`
  holds shared shape-coercion helpers (asObject/asString/asArray)
  and the OpenAPI-shape utilities (schemaRefName / describeType /
  summarizeInlineObject). Dependency direction: parser → extractors
  → internal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Reject `--operation` + `--format=openapi` upfront (typo'd op
  no longer silently produces full raw YAML).
- Debug-log when extractRequestBody skips a body-level `$ref`;
  the operation otherwise renders as bodyless without trace.
- Gate "Query params (required)" behind a length check so
  optional-only operations don't render a dead `(none)` block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…load helpers

Continues the architecture review's code-reduction pass:

- Drop `OperationJsonView` + `toOperationJsonView` from views.ts.
  Every field on the projection was already on `ParsedOperation`,
  so `buildSpecJsonView` now passes `operations` straight through.
  ~30 lines and one extra type gone.
- `commands/api/spec.ts`: use `docsUrlFor()` and the shared
  `loadSingleSpecOrThrow()` helper. The `loadSpec` private method
  and the local `HTTP_REFERENCE_BASE_URL` constant disappear.
- `commands/openapi/get.ts`: same — `docsUrlFor()` + the shared
  `DOCS_SERVICE_UNAVAILABLE` constant; local URL constant removed.
- Add `loadSingleSpecOrThrow()` next to `loadOperationsIndexOrThrow()`
  in parser.ts so single-spec consumers get the same friendly error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three additions in response to agent-discoverability review:

- The per-operation JSON exposed both `path` (`/jobs/{jobId}`) and
  `endpoint` (`v2021-06-07/jobs/:jobId`) — same shape, different
  conventions. Agents would reach for `path` and either hit a routing
  mismatch (it isn't callable) or syntax inconsistency. Rename to
  `openApiPath` to make the cross-reference role explicit.
- `--schema <name>` defaulted to YAML. The primary consumer following a
  `$ref` pointer is an agent — JSON parses without a YAML library.
  Switch the default to JSON; humans opt into YAML via `--format=yaml`.
  `--format=openapi --schema` was a redundant alias for the same YAML
  output and is dropped; `yaml` is the canonical opt-in.
- Clarify in `--web` help text that it's a human-only mode (no
  machine-readable output).

Also drops the redundant `optionalQueryParams: string[]` field on
`ParsedOperation` that the upstream parser rewrite made unnecessary —
the per-operation JSON now derives it from `queryParams` directly, and
list-row JSON falls through to `summary || description` when the spec
omits a summary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops spec.test.ts's local `JOBS_SPEC_YAML`, `mockIndexAndSpec`, and
`afterEach` cleanup in favor of the shared module from the list branch.
Enriches the shared Jobs spec with the path-param description, bearer-
auth, and 200-response shape that spec.test asserts against — the
fixture now covers what every consumer in the topic needs.

The two spec-specific YAMLs (REF_SPEC_YAML for `$ref` rendering,
OPTIONAL_QUERY_SPEC_YAML for the optional-only-params edge case) stay
local — they're not shared and inlining them keeps spec.test self-
explanatory for the cases that matter to it alone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gu-stav and others added 4 commits May 13, 2026 17:16
Two adjacent cleanups in the parser layer:

- Fold `api/internal.ts` (shape-coercion + small OpenAPI helpers) into
  `api/extractors.ts`. The seam was extracted for testability but its
  only consumer is `extractors`; the deletion-test concentrates nothing
  — the guards are 1–2 lines each and the real bug risk lives in how
  `walkProperty` / `flattenComposition` compose them, not in the
  guards themselves. One file deleted, one import surface gone.

- Enforce operationId uniqueness within a parsed spec. OpenAPI requires
  it, but validators are lenient; and `synthesizeOperationId` can
  silently collide with an authored id elsewhere in the same spec.
  Either case would silently route `--operation=<id>` lookups to
  whichever record sorted last. `assertUniqueOperationIds` throws
  with the colliding paths so `fetchAndParseEntry` can skip the spec
  cleanly (other specs still load).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`(agent-friendly)` and `(human-only; no machine output)` framed the
`--format=json` and `--web` flags as for one audience or the other.
Drops both: these flags are useful for anyone using the CLI.

Also rewords a stale "agents indexing by operationId" comment in the
parser to describe the actual invariant (every row stays uniquely
addressable by operationId) without singling out any caller, and
drops a stale "5-column human table" docblock the previous list
branch left behind.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Description: "human view (default)" was a leftover audience tag;
  reads as "structured view by default". Drops "public" qualifier
  (the topic implies it).
- Examples: "human-readable", "canonical spec source", "human-friendly"
  audience tags and unnecessary qualifications removed.
- `--format` description: dense one-liner mixing definitions with
  conditional behavior — split into one clause per format, surface
  the `yaml`-is-paired-with-`--schema` rule explicitly.
- `--schema` description: drop OpenAPI internal path syntax
  (`components.schemas.<name>`) from the user-facing hint; call it
  "a named component schema".
- `--operation` + `--format=openapi` error: "byte-for-byte
  passthrough" was implementation jargon. Replaced with concrete
  framing: "Cannot narrow --format=openapi output…".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- `--schema` lookup used `name in schemas`, which walks the prototype
  chain — `--schema toString` would slip past the not-found guard and
  render `Object.prototype.toString` as JSON `undefined`. Switched to
  `Object.hasOwn`. Test added.
- `--schema` + `--operation` and `--schema` + `--format=openapi` were
  silently accepted (the schema branch returns before either flag is
  consulted). Both combos now reject upfront with a clear error, the
  same shape as the existing `--operation` + `--format=openapi` guard.
  Two tests added.
- `extractResponses` parsed range status keys (`2XX`/`4XX`/`5XX`)
  through `parseInt` to single digits (`2`, `4`, `5`). Now requires a
  fully-numeric key and skips range keys with a debug log. Test added.
- Response sort treated `default` as `status: 0`, which sorted it
  *before* every specific code. Now sorts numeric statuses ascending
  with `default` last. Test added.
- Parse errors propagated through `loadSingleSpecOrThrow` were
  re-thrown as `DOCS_SERVICE_UNAVAILABLE`, so a spec with bad
  operationIds told the user the service was down. Split the loader:
  fetch errors are wrapped (service-unavailable), parse errors
  propagate with their original message. Test added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gu-stav gu-stav force-pushed the feat/api-command-spec-cmd branch from 6bf31f5 to f1e517f Compare May 13, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant