Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 20 additions & 11 deletions docs/@v2/commands/score.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,22 @@ The command identifies the operations with the lowest scores and provides reason

```bash
redocly score <api>
redocly score <api> [--format=<option>]
redocly score <api> [--format=<option>] [--suggestions]
```

## Options

| Option | Type | Description |
| -------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| api | string | **REQUIRED.** Path to the API description filename or alias that you want to score. Refer to [the API section](#specify-api) for more details. |
| --config | string | Specify path to the [configuration file](../configuration/index.md). |
| --format | string | Format for the output.<br />**Possible values:** `stylish`, `json`. Default value is `stylish`. |
| --operation-details | boolean | Print a per-operation metrics table sorted by property count. |
| --debug-operation-id | string | Print a detailed schema breakdown for a specific operation (by `operationId` or `METHOD /path`). |
| --help | boolean | Show help. |
| --lint-config | string | Specify the severity level for the configuration file. <br/> **Possible values:** `warn`, `error`, `off`. Default value is `warn`. |
| --version | boolean | Show version number. |
| Option | Type | Description |
| -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| api | string | **REQUIRED.** Path to the API description filename or alias that you want to score. Refer to [the API section](#specify-api) for more details. |
| --config | string | Specify path to the [configuration file](../configuration/index.md). |
| --format | string | Format for the output.<br />**Possible values:** `stylish`, `json`. Default value is `stylish`. |
| --operation-details | boolean | Print a per-operation metrics table sorted by property count. |
| --debug-operation-id | string | Print a detailed schema breakdown for a specific operation (by `operationId` or `METHOD /path`). |
| --suggestions | boolean | Append copy-paste prompts (for LLM-assisted editing) for each hotspot operation. Also adds a `suggestion` field per hotspot in JSON output. Default: `false`. |
| --help | boolean | Show help. |
| --lint-config | string | Specify the severity level for the configuration file. <br/> **Possible values:** `warn`, `error`, `off`. Default value is `warn`. |
| --version | boolean | Show version number. |

## Examples

Expand Down Expand Up @@ -131,3 +132,11 @@ redocly score openapi.yaml --format=json
The JSON output includes the full data: top-level scores, subscores, per-operation raw metrics, per-operation scores, dependency depths, and hotspot details with reasoning.

The JSON format is suitable for CI pipelines, quality gates, or feeding results into dashboards.

### Suggestions (LLM prompts)

With `--suggestions`, the command adds an **Agent prompts (copy/paste)** section after hotspots in stylish output. Each hotspot gets a fenced block containing a self-contained prompt you can paste into an assistant to improve that operation in your OpenAPI file.

With `--format=json` and `--suggestions`, each hotspot object includes a `suggestion` string (the same prompt text). Structured `issues` codes are not included in JSON output.

These prompts are advisory; review generated edits before committing them.
2 changes: 2 additions & 0 deletions packages/cli/src/commands/score/__tests__/hotspots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ describe('selectTopHotspots', () => {
const result = selectTopHotspots(docMetrics, opScores, new Map([['op1', 0]]));
expect(result.length).toBe(1);
expect(result[0].reasons).toContain('High parameter count (10)');
expect(result[0].issues.some((i) => i.code === 'high_parameter_count')).toBe(true);
expect(result[0].issues.map((i) => i.message)).toEqual(result[0].reasons);
});

it('detects deep schema nesting', () => {
Expand Down
79 changes: 79 additions & 0 deletions packages/cli/src/commands/score/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,83 @@ describe('handleScore', () => {
const output = mockOutput.mock.calls.map(([s]: [string]) => s).join('');
expect(output).toContain('Hotspot');
});

it('should strip issues from JSON hotspots by default', async () => {
mockedCollectMetrics.mockReturnValue({
metrics: makeDocumentMetrics(
new Map([
[
'complexOp',
makeTestMetrics({
path: '/complex',
method: 'post',
operationId: 'complexOp',
parameterCount: 10,
operationDescriptionPresent: false,
}),
],
])
),
debugLogs: [],
});

await handleScore(createArgs({ format: 'json' }));
const parsed = JSON.parse(mockOutput.mock.calls[0][0]);
expect(parsed.hotspots.length).toBeGreaterThan(0);
expect(parsed.hotspots[0].issues).toBeUndefined();
expect(parsed.hotspots[0].suggestion).toBeUndefined();
});

it('should add suggestion to JSON hotspots when suggestions is true', async () => {
mockedCollectMetrics.mockReturnValue({
metrics: makeDocumentMetrics(
new Map([
[
'complexOp',
makeTestMetrics({
path: '/complex',
method: 'post',
operationId: 'complexOp',
parameterCount: 10,
operationDescriptionPresent: false,
}),
],
])
),
debugLogs: [],
});

await handleScore(createArgs({ format: 'json', suggestions: true }));
const parsed = JSON.parse(mockOutput.mock.calls[0][0]);
expect(typeof parsed.hotspots[0].suggestion).toBe('string');
expect(parsed.hotspots[0].suggestion).toContain('test.yaml');
expect(parsed.hotspots[0].issues).toBeUndefined();
});

it('should print agent prompts section when suggestions is true', async () => {
mockedCollectMetrics.mockReturnValue({
metrics: makeDocumentMetrics(
new Map([
[
'complexOp',
makeTestMetrics({
path: '/complex',
method: 'post',
operationId: 'complexOp',
parameterCount: 10,
operationDescriptionPresent: false,
}),
],
])
),
debugLogs: [],
});

await handleScore(createArgs({ suggestions: true }));

const output = mockOutput.mock.calls.map(([s]: [string]) => s).join('');
expect(output).toContain('Agent prompts (copy/paste)');
expect(output).toContain('test.yaml');
expect(output).toContain('```');
});
});
46 changes: 46 additions & 0 deletions packages/cli/src/commands/score/__tests__/suggestions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { buildHotspotAgentPrompt } from '../suggestions.js';
import type { HotspotOperation } from '../types.js';

function makeHotspot(overrides: Partial<HotspotOperation> = {}): HotspotOperation {
return {
path: '/widgets',
method: 'post',
operationId: 'createWidget',
agentReadinessScore: 42.5,
reasons: ['High parameter count (9)', 'Missing operation description'],
issues: [
{ code: 'high_parameter_count', message: 'High parameter count (9)' },
{ code: 'missing_operation_description', message: 'Missing operation description' },
],
...overrides,
};
}

describe('buildHotspotAgentPrompt', () => {
it('includes document path, operation label, reasons, and focus guidance', () => {
const prompt = buildHotspotAgentPrompt('openapi/widgets.yaml', makeHotspot());
expect(prompt).toContain('openapi/widgets.yaml');
expect(prompt).toContain('POST /widgets');
expect(prompt).toContain('operationId: createWidget');
expect(prompt).toContain('High parameter count (9)');
expect(prompt).toContain('Missing operation description');
expect(prompt).toContain('Reduce or consolidate parameters');
expect(prompt).toContain('Add a concise `description`');
expect(prompt).toContain('42.5/100');
});

it('omits operationId clause when absent', () => {
const prompt = buildHotspotAgentPrompt(
'api.yaml',
makeHotspot({
operationId: undefined,
method: 'get',
reasons: ['Missing response examples'],
issues: [{ code: 'missing_response_examples', message: 'Missing response examples' }],
})
);
expect(prompt).toContain('GET /widgets');
expect(prompt).not.toContain('operationId:');
expect(prompt).toContain('example` or `examples`');
});
});
17 changes: 15 additions & 2 deletions packages/cli/src/commands/score/formatters/json.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { logger } from '@redocly/openapi-core';

import { buildHotspotAgentPrompt } from '../suggestions.js';
import type { ScoreResult } from '../types.js';

export function printScoreJson(result: ScoreResult): void {
export function printScoreJson(
result: ScoreResult,
apiPath: string,
includeSuggestions: boolean
): void {
const hotspots = result.hotspots.map((h) => {
const { issues: _issues, ...rest } = h;
if (!includeSuggestions) {
return rest;
}
return { ...rest, suggestion: buildHotspotAgentPrompt(apiPath, h) };
});

const output = {
agentReadiness: result.agentReadiness,
discoverability: Math.round(result.discoverability * 100),
Expand All @@ -18,7 +31,7 @@ export function printScoreJson(result: ScoreResult): void {
},
operationScores: Object.fromEntries(result.operationScores),
dependencyDepths: Object.fromEntries(result.dependencyDepths),
hotspots: result.hotspots,
hotspots,
};

logger.output(JSON.stringify(output, null, 2));
Expand Down
32 changes: 31 additions & 1 deletion packages/cli/src/commands/score/formatters/stylish.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { logger } from '@redocly/openapi-core';
import { bold, cyan, green, red, white, yellow } from 'colorette';

import { buildHotspotAgentPrompt } from '../suggestions.js';
import type { DebugMediaTypeLog, ScoreResult } from '../types.js';
import { median } from '../utils.js';

export function printScoreStylish(result: ScoreResult, operationDetails = false): void {
export function printScoreStylish(
result: ScoreResult,
operationDetails = false,
apiPath = '',
includeSuggestions = false
): void {
printScores(result);
printSubscores(result);
printRawMetricsSummary(result);
if (operationDetails) printOperationDetails(result);
printHotspots(result);
if (includeSuggestions && apiPath) {
printAgentPrompts(apiPath, result);
}
}

function scoreColor(score: number): (text: string) => string {
Expand Down Expand Up @@ -275,3 +284,24 @@ function printHotspots(result: ScoreResult): void {
out('');
}
}

function printAgentPrompts(apiPath: string, result: ScoreResult): void {
if (result.hotspots.length === 0) {
return;
}

out('');
out(bold(white(' Agent prompts (copy/paste)')));
out('');

for (const hotspot of result.hotspots) {
const prompt = buildHotspotAgentPrompt(apiPath, hotspot);
out('```');
const lines = prompt.split('\n');
for (const line of lines) {
out(line);
}
out('```');
out('');
}
}
Loading
Loading