From 40f2bfd5a0457d63b6bc0fafd776dfcd4c5d95fc Mon Sep 17 00:00:00 2001 From: Jeff <158072326+jeffrey701@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:30:05 +0200 Subject: [PATCH] docs(openapi): document the /v1/lint/* + /v1/validate/focus-manifest route family in the spec --- apps/loopover-ui/public/openapi.json | 738 +++++++++++++++++++++++++++ src/openapi/schemas.ts | 178 +++++++ src/openapi/spec.ts | 136 +++++ test/unit/openapi.test.ts | 84 +++ 4 files changed, 1136 insertions(+) diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json index 0dc4a98dec..f2ae0382d6 100644 --- a/apps/loopover-ui/public/openapi.json +++ b/apps/loopover-ui/public/openapi.json @@ -15590,6 +15590,448 @@ ] } } + }, + "LintPrTextRequest": { + "type": "object", + "properties": { + "commitMessages": { + "type": "array", + "items": { + "type": "string" + } + }, + "prBody": { + "type": "string" + }, + "linkedIssue": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + } + } + }, + "LintPrTextResponse": { + "type": "object", + "properties": { + "verdict": { + "type": "string" + }, + "score": { + "type": "number" + }, + "components": { + "nullable": true + }, + "fixes": { + "nullable": true + }, + "summary": { + "type": "string" + }, + "generatedAt": { + "type": "string" + } + } + }, + "CheckSlopRiskRequest": { + "type": "object", + "properties": { + "changedFiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "additions": { + "type": "integer" + }, + "deletions": { + "type": "integer" + } + }, + "required": [ + "path" + ] + } + }, + "description": { + "type": "string" + }, + "tests": { + "type": "array", + "items": { + "type": "string" + } + }, + "testFiles": { + "type": "array", + "items": { + "type": "string" + } + }, + "commitMessages": { + "type": "array", + "items": { + "type": "string" + } + }, + "hasLinkedIssue": { + "type": "boolean" + }, + "issueDiscoveryLane": { + "type": "boolean" + } + }, + "required": [ + "changedFiles" + ] + }, + "CheckSlopRiskResponse": { + "type": "object", + "properties": { + "slopRisk": { + "type": "number" + }, + "band": { + "type": "string", + "enum": [ + "clean", + "low", + "elevated", + "high" + ] + }, + "findings": { + "nullable": true + }, + "rubric": { + "type": "string" + } + } + }, + "CheckImprovementPotentialRequest": { + "type": "object", + "properties": { + "changedFiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "additions": { + "type": "integer" + }, + "deletions": { + "type": "integer" + } + }, + "required": [ + "path" + ] + } + }, + "tests": { + "type": "array", + "items": { + "type": "string" + } + }, + "testFiles": { + "type": "array", + "items": { + "type": "string" + } + }, + "patchCoverageDeltaPercent": { + "type": "number" + }, + "complexityDeltas": { + "type": "array", + "items": { + "nullable": true + } + }, + "duplicationDeltas": { + "type": "array", + "items": { + "nullable": true + } + } + } + }, + "CheckImprovementPotentialResponse": { + "type": "object", + "properties": { + "improvementScore": { + "type": "number" + }, + "band": { + "type": "string", + "enum": [ + "insufficient-signal", + "none", + "minor", + "moderate", + "significant" + ] + }, + "findings": { + "nullable": true + } + } + }, + "SimulateOpenPrPressureRequest": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "generatedAt": { + "type": "string" + }, + "queueHealth": { + "nullable": true + }, + "roleContext": { + "type": "object", + "properties": { + "maintainerLane": { + "type": "boolean" + } + }, + "required": [ + "maintainerLane" + ] + }, + "contributorOpenPrCount": { + "type": "integer" + } + }, + "required": [ + "repoFullName", + "generatedAt", + "roleContext" + ] + }, + "SimulateOpenPrPressureResponse": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "generatedAt": { + "type": "string" + }, + "lane": { + "type": "string" + }, + "queuePressure": { + "type": "string" + }, + "recommendedOption": { + "type": "string" + }, + "scenarios": { + "type": "array", + "items": { + "nullable": true + } + }, + "summary": { + "type": "string" + } + } + }, + "SuggestBoundaryTestsRequest": { + "type": "object", + "properties": { + "changedFiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] + } + }, + "boundaryTouches": { + "type": "array", + "items": { + "nullable": true + } + }, + "tests": { + "type": "array", + "items": { + "type": "string" + } + }, + "testFiles": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "changedFiles" + ] + }, + "SuggestBoundaryTestsResponse": { + "type": "object", + "properties": { + "finding": { + "nullable": true + }, + "spec": { + "nullable": true + } + } + }, + "CheckTestEvidenceRequest": { + "type": "object", + "properties": { + "changedPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "testFiles": { + "type": "array", + "items": { + "type": "string" + } + }, + "tests": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "changedPaths" + ] + }, + "CheckTestEvidenceResponse": { + "type": "object", + "properties": { + "classification": { + "type": "string", + "enum": [ + "strong", + "adequate", + "weak", + "absent" + ] + }, + "changedFileCount": { + "type": "number" + }, + "codeFileCount": { + "type": "number" + }, + "testFileCount": { + "type": "number" + }, + "guidance": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CheckIssueSlopRequest": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "type": "string" + } + } + }, + "CheckIssueSlopResponse": { + "type": "object", + "properties": { + "slopRisk": { + "type": "number" + }, + "band": { + "type": "string", + "enum": [ + "clean", + "low", + "elevated", + "high" + ] + }, + "findings": { + "nullable": true + }, + "rubric": { + "type": "string" + } + } + }, + "ValidateFocusManifestRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "repo_file", + "api_record", + "none" + ] + } + }, + "required": [ + "content" + ] + }, + "ValidateFocusManifestResponse": { + "type": "object", + "properties": { + "present": { + "type": "boolean" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "normalized": { + "type": "object", + "additionalProperties": { + "nullable": true + } + }, + "status": { + "type": "string", + "enum": [ + "ok", + "warn", + "error" + ] + } + } } }, "parameters": {}, @@ -20663,6 +21105,302 @@ } ] } + }, + "/v1/lint/pr-text": { + "post": { + "summary": "Lint a PR's commit messages + body — REST mirror of loopover_lint_pr_text (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LintPrTextRequest" + } + } + } + }, + "responses": { + "200": { + "description": "PR-text lint verdict, score, and fix suggestions — mirrors the loopover_lint_pr_text MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LintPrTextResponse" + } + } + } + }, + "400": { + "description": "Invalid lint/pr-text request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/lint/slop-risk": { + "post": { + "summary": "Assess a changeset's slop risk — REST mirror of loopover_check_slop_risk (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckSlopRiskRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Slop-risk band and findings over the caller-supplied changed files — mirrors the loopover_check_slop_risk MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckSlopRiskResponse" + } + } + } + }, + "400": { + "description": "Invalid lint/slop-risk request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/lint/improvement-potential": { + "post": { + "summary": "Score a changeset's structural improvement potential — REST mirror of loopover_check_improvement_potential (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImprovementPotentialRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Improvement-potential score, band, and findings — mirrors the loopover_check_improvement_potential MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckImprovementPotentialResponse" + } + } + } + }, + "400": { + "description": "Invalid lint/improvement-potential request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/lint/open-pr-pressure": { + "post": { + "summary": "Simulate open-PR queue pressure for a repo — REST mirror of loopover_simulate_open_pr_pressure (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimulateOpenPrPressureRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Queue-pressure scenarios and the recommended option over caller-supplied queue health — mirrors the loopover_simulate_open_pr_pressure MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimulateOpenPrPressureResponse" + } + } + } + }, + "400": { + "description": "Invalid lint/open-pr-pressure request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/lint/boundary-tests": { + "post": { + "summary": "Suggest boundary tests for a changeset — REST mirror of loopover_suggest_boundary_tests (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuggestBoundaryTestsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Boundary-test finding and suggested test spec — mirrors the loopover_suggest_boundary_tests MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuggestBoundaryTestsResponse" + } + } + } + }, + "400": { + "description": "Invalid lint/boundary-tests request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/lint/test-evidence": { + "post": { + "summary": "Classify a changeset's test evidence — REST mirror of loopover_check_test_evidence (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckTestEvidenceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Test-evidence classification, file counts, and guidance over caller-supplied changed paths — mirrors the loopover_check_test_evidence MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckTestEvidenceResponse" + } + } + } + }, + "400": { + "description": "Invalid lint/test-evidence request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/lint/issue-slop": { + "post": { + "summary": "Assess an issue's slop risk from its title/body — REST mirror of loopover_check_issue_slop (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckIssueSlopRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Slop-risk band and findings over the caller-supplied issue title/body — mirrors the loopover_check_issue_slop MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckIssueSlopResponse" + } + } + } + }, + "400": { + "description": "Invalid lint/issue-slop request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/validate/focus-manifest": { + "post": { + "summary": "Validate a .loopover focus-manifest config — REST mirror of loopover_validate_config (#9308)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateFocusManifestRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Focus-manifest presence, normalized config, warnings, and status — mirrors the loopover_validate_config MCP tool", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateFocusManifestResponse" + } + } + } + }, + "400": { + "description": "Invalid validate/focus-manifest request body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } } }, "servers": [ diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 3816a94379..6a7a2b823b 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -3243,3 +3243,181 @@ export const McpCompatibilitySchema = z generatedAt: z.string(), }) .openapi("McpCompatibility"); + +// #9308: request/response bodies for the eight deterministic advisory-check routes under /v1/lint/* and +// /v1/validate/focus-manifest. Each mirrors the top-level keys of the corresponding MCP tool +// inputSchema/outputSchema in src/mcp/server.ts (the source of truth), matching the #9309 loop-route pattern +// above. Deeply nested / opaque payloads are left as `unknown` (the routes pass caller-supplied structures +// through or the tool composes them), exactly as the #9309 schemas do for their opaque fields. + +const ChangedFileSchema = z.object({ + path: z.string(), + additions: z.number().int().optional(), + deletions: z.number().int().optional(), +}); + +/** Request body for POST /v1/lint/pr-text — parity with `lintPrTextShape` (loopover_lint_pr_text). */ +export const LintPrTextRequestSchema = z + .object({ + commitMessages: z.array(z.string()).optional(), + prBody: z.string().optional(), + linkedIssue: z.number().int().positive().optional(), + }) + .openapi("LintPrTextRequest"); + +/** Response body for POST /v1/lint/pr-text — parity with `lintPrTextOutputSchema`. */ +export const LintPrTextResponseSchema = z + .object({ + verdict: z.string().optional(), + score: z.number().optional(), + components: z.unknown().optional(), + fixes: z.unknown().optional(), + summary: z.string().optional(), + generatedAt: z.string().optional(), + }) + .openapi("LintPrTextResponse"); + +/** Request body for POST /v1/lint/slop-risk — parity with `checkSlopRiskShape` (loopover_check_slop_risk). */ +export const CheckSlopRiskRequestSchema = z + .object({ + changedFiles: z.array(ChangedFileSchema), + description: z.string().optional(), + tests: z.array(z.string()).optional(), + testFiles: z.array(z.string()).optional(), + commitMessages: z.array(z.string()).optional(), + hasLinkedIssue: z.boolean().optional(), + issueDiscoveryLane: z.boolean().optional(), + }) + .openapi("CheckSlopRiskRequest"); + +/** Response body for POST /v1/lint/slop-risk — parity with `checkSlopRiskOutputSchema`. */ +export const CheckSlopRiskResponseSchema = z + .object({ + slopRisk: z.number().optional(), + band: z.enum(["clean", "low", "elevated", "high"]).optional(), + findings: z.unknown().optional(), + rubric: z.string().optional(), + }) + .openapi("CheckSlopRiskResponse"); + +/** Request body for POST /v1/lint/improvement-potential — parity with `checkImprovementPotentialShape`. */ +export const CheckImprovementPotentialRequestSchema = z + .object({ + changedFiles: z.array(ChangedFileSchema).optional(), + tests: z.array(z.string()).optional(), + testFiles: z.array(z.string()).optional(), + patchCoverageDeltaPercent: z.number().optional(), + complexityDeltas: z.array(z.unknown()).optional(), + duplicationDeltas: z.array(z.unknown()).optional(), + }) + .openapi("CheckImprovementPotentialRequest"); + +/** Response body for POST /v1/lint/improvement-potential — parity with `checkImprovementPotentialOutputSchema`. */ +export const CheckImprovementPotentialResponseSchema = z + .object({ + improvementScore: z.number().optional(), + band: z.enum(["insufficient-signal", "none", "minor", "moderate", "significant"]).optional(), + findings: z.unknown().optional(), + }) + .openapi("CheckImprovementPotentialResponse"); + +/** Request body for POST /v1/lint/open-pr-pressure — parity with `simulateOpenPrPressureShape`. */ +export const SimulateOpenPrPressureRequestSchema = z + .object({ + repoFullName: z.string(), + generatedAt: z.string(), + queueHealth: z.unknown(), + roleContext: z.object({ maintainerLane: z.boolean() }), + contributorOpenPrCount: z.number().int().optional(), + }) + .openapi("SimulateOpenPrPressureRequest"); + +/** Response body for POST /v1/lint/open-pr-pressure — parity with `simulateOpenPrPressureOutputSchema`. */ +export const SimulateOpenPrPressureResponseSchema = z + .object({ + repoFullName: z.string().optional(), + generatedAt: z.string().optional(), + lane: z.string().optional(), + queuePressure: z.string().optional(), + recommendedOption: z.string().optional(), + scenarios: z.array(z.unknown()).optional(), + summary: z.string().optional(), + }) + .openapi("SimulateOpenPrPressureResponse"); + +/** Request body for POST /v1/lint/boundary-tests — parity with `suggestBoundaryTestsShape`. */ +export const SuggestBoundaryTestsRequestSchema = z + .object({ + changedFiles: z.array(z.object({ path: z.string() })), + boundaryTouches: z.array(z.unknown()).optional(), + tests: z.array(z.string()).optional(), + testFiles: z.array(z.string()).optional(), + }) + .openapi("SuggestBoundaryTestsRequest"); + +/** Response body for POST /v1/lint/boundary-tests — parity with `suggestBoundaryTestsOutputSchema`. */ +export const SuggestBoundaryTestsResponseSchema = z + .object({ + finding: z.unknown().optional(), + spec: z.unknown().optional(), + }) + .openapi("SuggestBoundaryTestsResponse"); + +/** Request body for POST /v1/lint/test-evidence — parity with `checkTestEvidenceShape`. */ +export const CheckTestEvidenceRequestSchema = z + .object({ + changedPaths: z.array(z.string()), + testFiles: z.array(z.string()).optional(), + tests: z.array(z.string()).optional(), + }) + .openapi("CheckTestEvidenceRequest"); + +/** Response body for POST /v1/lint/test-evidence — parity with `checkTestEvidenceOutputSchema`. */ +export const CheckTestEvidenceResponseSchema = z + .object({ + classification: z.enum(["strong", "adequate", "weak", "absent"]).optional(), + changedFileCount: z.number().optional(), + codeFileCount: z.number().optional(), + testFileCount: z.number().optional(), + guidance: z.array(z.string()).optional(), + }) + .openapi("CheckTestEvidenceResponse"); + +/** Request body for POST /v1/lint/issue-slop — parity with `checkIssueSlopShape` (loopover_check_issue_slop). */ +export const CheckIssueSlopRequestSchema = z + .object({ + title: z.string().optional(), + body: z.string().optional(), + }) + .openapi("CheckIssueSlopRequest"); + +/** + * Response body for POST /v1/lint/issue-slop — parity with `checkIssueSlopOutputSchema`, which is an alias of + * `checkSlopRiskOutputSchema` in src/mcp/server.ts, so the keys match CheckSlopRiskResponse. + */ +export const CheckIssueSlopResponseSchema = z + .object({ + slopRisk: z.number().optional(), + band: z.enum(["clean", "low", "elevated", "high"]).optional(), + findings: z.unknown().optional(), + rubric: z.string().optional(), + }) + .openapi("CheckIssueSlopResponse"); + +/** Request body for POST /v1/validate/focus-manifest — parity with `validateConfigShape` (loopover_validate_config). */ +export const ValidateFocusManifestRequestSchema = z + .object({ + content: z.string(), + source: z.enum(["repo_file", "api_record", "none"]).optional(), + }) + .openapi("ValidateFocusManifestRequest"); + +/** Response body for POST /v1/validate/focus-manifest — parity with `validateConfigOutputSchema`. */ +export const ValidateFocusManifestResponseSchema = z + .object({ + present: z.boolean().optional(), + warnings: z.array(z.string()).optional(), + normalized: z.record(z.string(), z.unknown()).optional(), + status: z.enum(["ok", "warn", "error"]).optional(), + }) + .openapi("ValidateFocusManifestResponse"); diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index cf1df39aa3..ffe1ba3ff4 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -49,6 +49,22 @@ import { IntakeIdeaResponseSchema, PlanIdeaClaimsRequestSchema, PlanIdeaClaimsResponseSchema, + LintPrTextRequestSchema, + LintPrTextResponseSchema, + CheckSlopRiskRequestSchema, + CheckSlopRiskResponseSchema, + CheckImprovementPotentialRequestSchema, + CheckImprovementPotentialResponseSchema, + SimulateOpenPrPressureRequestSchema, + SimulateOpenPrPressureResponseSchema, + SuggestBoundaryTestsRequestSchema, + SuggestBoundaryTestsResponseSchema, + CheckTestEvidenceRequestSchema, + CheckTestEvidenceResponseSchema, + CheckIssueSlopRequestSchema, + CheckIssueSlopResponseSchema, + ValidateFocusManifestRequestSchema, + ValidateFocusManifestResponseSchema, LabelAuditSchema, LaneAdviceSchema, LiveGateThresholdsResponseSchema, @@ -196,6 +212,22 @@ export function buildOpenApiSpec() { registry.register("IntakeIdeaResponse", IntakeIdeaResponseSchema); registry.register("PlanIdeaClaimsRequest", PlanIdeaClaimsRequestSchema); registry.register("PlanIdeaClaimsResponse", PlanIdeaClaimsResponseSchema); + registry.register("LintPrTextRequest", LintPrTextRequestSchema); + registry.register("LintPrTextResponse", LintPrTextResponseSchema); + registry.register("CheckSlopRiskRequest", CheckSlopRiskRequestSchema); + registry.register("CheckSlopRiskResponse", CheckSlopRiskResponseSchema); + registry.register("CheckImprovementPotentialRequest", CheckImprovementPotentialRequestSchema); + registry.register("CheckImprovementPotentialResponse", CheckImprovementPotentialResponseSchema); + registry.register("SimulateOpenPrPressureRequest", SimulateOpenPrPressureRequestSchema); + registry.register("SimulateOpenPrPressureResponse", SimulateOpenPrPressureResponseSchema); + registry.register("SuggestBoundaryTestsRequest", SuggestBoundaryTestsRequestSchema); + registry.register("SuggestBoundaryTestsResponse", SuggestBoundaryTestsResponseSchema); + registry.register("CheckTestEvidenceRequest", CheckTestEvidenceRequestSchema); + registry.register("CheckTestEvidenceResponse", CheckTestEvidenceResponseSchema); + registry.register("CheckIssueSlopRequest", CheckIssueSlopRequestSchema); + registry.register("CheckIssueSlopResponse", CheckIssueSlopResponseSchema); + registry.register("ValidateFocusManifestRequest", ValidateFocusManifestRequestSchema); + registry.register("ValidateFocusManifestResponse", ValidateFocusManifestResponseSchema); registry.register("LiveGateThresholdsResponse", LiveGateThresholdsResponseSchema); registry.register("BurdenForecast", BurdenForecastSchema); registry.register("ContributorScoringProfile", ContributorScoringProfileSchema); @@ -616,6 +648,110 @@ export function buildOpenApiSpec() { 400: { description: "Invalid plan-idea-claims request body, or an empty/malformed idea submission (actionable error list returned)" }, }, }); + registry.registerPath({ + method: "post", + path: "/v1/lint/pr-text", + summary: "Lint a PR's commit messages + body — REST mirror of loopover_lint_pr_text (#9308)", + request: { body: { content: { "application/json": { schema: LintPrTextRequestSchema } } } }, + responses: { + 200: { + description: "PR-text lint verdict, score, and fix suggestions — mirrors the loopover_lint_pr_text MCP tool", + content: { "application/json": { schema: LintPrTextResponseSchema } }, + }, + 400: { description: "Invalid lint/pr-text request body" }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/lint/slop-risk", + summary: "Assess a changeset's slop risk — REST mirror of loopover_check_slop_risk (#9308)", + request: { body: { content: { "application/json": { schema: CheckSlopRiskRequestSchema } } } }, + responses: { + 200: { + description: "Slop-risk band and findings over the caller-supplied changed files — mirrors the loopover_check_slop_risk MCP tool", + content: { "application/json": { schema: CheckSlopRiskResponseSchema } }, + }, + 400: { description: "Invalid lint/slop-risk request body" }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/lint/improvement-potential", + summary: "Score a changeset's structural improvement potential — REST mirror of loopover_check_improvement_potential (#9308)", + request: { body: { content: { "application/json": { schema: CheckImprovementPotentialRequestSchema } } } }, + responses: { + 200: { + description: "Improvement-potential score, band, and findings — mirrors the loopover_check_improvement_potential MCP tool", + content: { "application/json": { schema: CheckImprovementPotentialResponseSchema } }, + }, + 400: { description: "Invalid lint/improvement-potential request body" }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/lint/open-pr-pressure", + summary: "Simulate open-PR queue pressure for a repo — REST mirror of loopover_simulate_open_pr_pressure (#9308)", + request: { body: { content: { "application/json": { schema: SimulateOpenPrPressureRequestSchema } } } }, + responses: { + 200: { + description: "Queue-pressure scenarios and the recommended option over caller-supplied queue health — mirrors the loopover_simulate_open_pr_pressure MCP tool", + content: { "application/json": { schema: SimulateOpenPrPressureResponseSchema } }, + }, + 400: { description: "Invalid lint/open-pr-pressure request body" }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/lint/boundary-tests", + summary: "Suggest boundary tests for a changeset — REST mirror of loopover_suggest_boundary_tests (#9308)", + request: { body: { content: { "application/json": { schema: SuggestBoundaryTestsRequestSchema } } } }, + responses: { + 200: { + description: "Boundary-test finding and suggested test spec — mirrors the loopover_suggest_boundary_tests MCP tool", + content: { "application/json": { schema: SuggestBoundaryTestsResponseSchema } }, + }, + 400: { description: "Invalid lint/boundary-tests request body" }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/lint/test-evidence", + summary: "Classify a changeset's test evidence — REST mirror of loopover_check_test_evidence (#9308)", + request: { body: { content: { "application/json": { schema: CheckTestEvidenceRequestSchema } } } }, + responses: { + 200: { + description: "Test-evidence classification, file counts, and guidance over caller-supplied changed paths — mirrors the loopover_check_test_evidence MCP tool", + content: { "application/json": { schema: CheckTestEvidenceResponseSchema } }, + }, + 400: { description: "Invalid lint/test-evidence request body" }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/lint/issue-slop", + summary: "Assess an issue's slop risk from its title/body — REST mirror of loopover_check_issue_slop (#9308)", + request: { body: { content: { "application/json": { schema: CheckIssueSlopRequestSchema } } } }, + responses: { + 200: { + description: "Slop-risk band and findings over the caller-supplied issue title/body — mirrors the loopover_check_issue_slop MCP tool", + content: { "application/json": { schema: CheckIssueSlopResponseSchema } }, + }, + 400: { description: "Invalid lint/issue-slop request body" }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/validate/focus-manifest", + summary: "Validate a .loopover focus-manifest config — REST mirror of loopover_validate_config (#9308)", + request: { body: { content: { "application/json": { schema: ValidateFocusManifestRequestSchema } } } }, + responses: { + 200: { + description: "Focus-manifest presence, normalized config, warnings, and status — mirrors the loopover_validate_config MCP tool", + content: { "application/json": { schema: ValidateFocusManifestResponseSchema } }, + }, + 400: { description: "Invalid validate/focus-manifest request body" }, + }, + }); registry.registerPath({ method: "get", path: "/v1/repos/{owner}/{repo}/live-gate-thresholds", diff --git a/test/unit/openapi.test.ts b/test/unit/openapi.test.ts index c5eb0da308..7e1aa5068e 100644 --- a/test/unit/openapi.test.ts +++ b/test/unit/openapi.test.ts @@ -277,6 +277,90 @@ describe("OpenAPI contract", () => { expect(spec.paths["/v1/loop/request-apr-transfer"]).toBeUndefined(); }); + // #9308: the eight /v1/lint/* + /v1/validate/focus-manifest advisory-check routes are each backed by an MCP + // tool whose Zod input/output shapes live in src/mcp/server.ts. Assert every route is a documented POST path + // whose request/response components expose exactly the tool's top-level keys (the MCP table in #9308), so a + // future field added to a tool but not the spec (or a mis-registered component) fails loudly here. + it("documents the /v1/lint/* + /v1/validate/focus-manifest advisory-check family with tool-parity schemas (#9308)", () => { + const spec = buildOpenApiSpec(); + const schemas = spec.components?.schemas ?? {}; + + const propKeys = (name: string) => + Object.keys((schemas[name] as { properties?: Record }).properties ?? {}).sort(); + + const cases = [ + { + path: "/v1/lint/pr-text", + request: "LintPrTextRequest", + response: "LintPrTextResponse", + inputKeys: ["commitMessages", "prBody", "linkedIssue"], + outputKeys: ["verdict", "score", "components", "fixes", "summary", "generatedAt"], + }, + { + path: "/v1/lint/slop-risk", + request: "CheckSlopRiskRequest", + response: "CheckSlopRiskResponse", + inputKeys: ["changedFiles", "description", "tests", "testFiles", "commitMessages", "hasLinkedIssue", "issueDiscoveryLane"], + outputKeys: ["slopRisk", "band", "findings", "rubric"], + }, + { + path: "/v1/lint/improvement-potential", + request: "CheckImprovementPotentialRequest", + response: "CheckImprovementPotentialResponse", + inputKeys: ["changedFiles", "tests", "testFiles", "patchCoverageDeltaPercent", "complexityDeltas", "duplicationDeltas"], + outputKeys: ["improvementScore", "band", "findings"], + }, + { + path: "/v1/lint/open-pr-pressure", + request: "SimulateOpenPrPressureRequest", + response: "SimulateOpenPrPressureResponse", + inputKeys: ["repoFullName", "generatedAt", "queueHealth", "roleContext", "contributorOpenPrCount"], + outputKeys: ["repoFullName", "generatedAt", "lane", "queuePressure", "recommendedOption", "scenarios", "summary"], + }, + { + path: "/v1/lint/boundary-tests", + request: "SuggestBoundaryTestsRequest", + response: "SuggestBoundaryTestsResponse", + inputKeys: ["changedFiles", "boundaryTouches", "tests", "testFiles"], + outputKeys: ["finding", "spec"], + }, + { + path: "/v1/lint/test-evidence", + request: "CheckTestEvidenceRequest", + response: "CheckTestEvidenceResponse", + inputKeys: ["changedPaths", "testFiles", "tests"], + outputKeys: ["classification", "changedFileCount", "codeFileCount", "testFileCount", "guidance"], + }, + { + // issue-slop's MCP output is an alias of checkSlopRiskOutputSchema, so its keys match slop-risk's response. + path: "/v1/lint/issue-slop", + request: "CheckIssueSlopRequest", + response: "CheckIssueSlopResponse", + inputKeys: ["title", "body"], + outputKeys: ["slopRisk", "band", "findings", "rubric"], + }, + { + path: "/v1/validate/focus-manifest", + request: "ValidateFocusManifestRequest", + response: "ValidateFocusManifestResponse", + inputKeys: ["content", "source"], + outputKeys: ["present", "warnings", "normalized", "status"], + }, + ]; + + for (const { path, request, response, inputKeys, outputKeys } of cases) { + const op = spec.paths[path]?.post; + expect(op, `${path} should be a documented POST path`).toBeDefined(); + expect(op?.requestBody, `${path} should document a request body`).toBeDefined(); + + expect(schemas[request], `${request} component should be registered`).toBeDefined(); + expect(schemas[response], `${response} component should be registered`).toBeDefined(); + + expect(propKeys(request)).toEqual([...inputKeys].sort()); + expect(propKeys(response)).toEqual([...outputKeys].sort()); + } + }); + // #9301: the two /v1/scoring/* composer routes backed by loopover_get_eligibility_plan and // loopover_explain_score_breakdown. Assert each is a documented POST path whose 200 response // component stays field-for-field in parity with the MCP tool outputSchema.