Skip to content
Merged
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
112 changes: 112 additions & 0 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -15256,6 +15256,62 @@
"required": [
"ok"
]
},
"EligibilityPlanResponse": {
"type": "object",
"properties": {
"eligible": {
"type": "boolean"
},
"linkedIssueStatus": {
"type": "string"
},
"branchEligibilityStatus": {
"type": "string"
},
"blockers": {
"type": "array",
"items": {
"type": "string"
}
},
"cleanupPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"linkedIssueProjection": {
"type": "string",
"nullable": true
},
"publicSummary": {
"type": "string"
}
}
},
"ScoreBreakdownResponse": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"scoreabilityStatus": {
"type": "string"
},
"effectiveEstimatedScore": {
"type": "number"
},
"components": {
"nullable": true
},
"gateHighlights": {
"nullable": true
},
"highestLeverageLever": {
"nullable": true
}
}
}
},
"parameters": {},
Expand Down Expand Up @@ -20022,6 +20078,62 @@
}
]
}
},
"/v1/scoring/eligibility-plan": {
"post": {
"summary": "Derive a contributor eligibility plan from a scoring preview — REST mirror of loopover_get_eligibility_plan (#9301)",
"responses": {
"200": {
"description": "Structured eligibility plan over a server-built score preview — mirrors the loopover_get_eligibility_plan MCP tool. Advisory only; it explains eligibility, it does not open issues or PRs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EligibilityPlanResponse"
}
}
}
},
"400": {
"description": "Invalid scoring preview input"
}
},
"security": [
{
"LoopOverBearer": []
},
{
"LoopOverSessionCookie": []
}
]
}
},
"/v1/scoring/explain-breakdown": {
"post": {
"summary": "Explain a score breakdown from a scoring preview — REST mirror of loopover_explain_score_breakdown (#9301)",
"responses": {
"200": {
"description": "Score multiplier breakdown and gate highlights over a server-built score preview — mirrors the loopover_explain_score_breakdown MCP tool. Requires contributorLogin in the request body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScoreBreakdownResponse"
}
}
}
},
"400": {
"description": "Invalid scoring preview input or missing contributorLogin"
}
},
"security": [
{
"LoopOverBearer": []
},
{
"LoopOverSessionCookie": []
}
]
}
}
},
"servers": [
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1697,7 +1697,7 @@ const remediationPlanOutputSchema = {
items: z.unknown().optional(),
};

const scoreBreakdownOutputSchema = {
export const scoreBreakdownOutputSchema = {
repoFullName: z.string().optional(),
scoreabilityStatus: z.string().optional(),
effectiveEstimatedScore: z.number().optional(),
Expand All @@ -1706,7 +1706,7 @@ const scoreBreakdownOutputSchema = {
highestLeverageLever: z.unknown().optional(),
};

const eligibilityPlanOutputSchema = {
export const eligibilityPlanOutputSchema = {
eligible: z.boolean().optional(),
linkedIssueStatus: z.string().optional(),
branchEligibilityStatus: z.string().optional(),
Expand Down
31 changes: 31 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,37 @@ export const GateConfigEffectiveResponseSchema = z
})
.openapi("GateConfigEffectiveResponse");

/**
* Response body for POST /v1/scoring/eligibility-plan. Field-level parity with `eligibilityPlanOutputSchema`
* (the `loopover_get_eligibility_plan` MCP tool `outputSchema`) in src/mcp/server.ts — #9301.
*/
export const EligibilityPlanResponseSchema = z
.object({
eligible: z.boolean().optional(),
linkedIssueStatus: z.string().optional(),
branchEligibilityStatus: z.string().optional(),
blockers: z.array(z.string()).optional(),
cleanupPaths: z.array(z.string()).optional(),
linkedIssueProjection: z.string().nullable().optional(),
publicSummary: z.string().optional(),
})
.openapi("EligibilityPlanResponse");

/**
* Response body for POST /v1/scoring/explain-breakdown. Field-level parity with `scoreBreakdownOutputSchema`
* (the `loopover_explain_score_breakdown` MCP tool `outputSchema`) in src/mcp/server.ts — #9301.
*/
export const ScoreBreakdownResponseSchema = z
.object({
repoFullName: z.string().optional(),
scoreabilityStatus: z.string().optional(),
effectiveEstimatedScore: z.number().optional(),
components: z.unknown().optional(),
gateHighlights: z.unknown().optional(),
highestLeverageLever: z.unknown().optional(),
})
.openapi("ScoreBreakdownResponse");

/**
* Request body for POST /v1/loop/evaluate-escalation. Field-level parity with `evaluateEscalationShape`
* (the `loopover_evaluate_escalation` MCP tool `inputSchema`) in src/mcp/server.ts — #9309.
Expand Down
30 changes: 30 additions & 0 deletions src/openapi/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
IssueQualityReportSchema,
IssueQualityResponseSchema,
GateConfigEffectiveResponseSchema,
EligibilityPlanResponseSchema,
ScoreBreakdownResponseSchema,
EvaluateEscalationRequestSchema,
EvaluateEscalationResponseSchema,
BuildResultsPayloadRequestSchema,
Expand Down Expand Up @@ -173,6 +175,8 @@ export function buildOpenApiSpec() {
registry.register("IssueQualityReport", IssueQualityReportSchema);
registry.register("IssueQualityResponse", IssueQualityResponseSchema);
registry.register("GateConfigEffectiveResponse", GateConfigEffectiveResponseSchema);
registry.register("EligibilityPlanResponse", EligibilityPlanResponseSchema);
registry.register("ScoreBreakdownResponse", ScoreBreakdownResponseSchema);
registry.register("EvaluateEscalationRequest", EvaluateEscalationRequestSchema);
registry.register("EvaluateEscalationResponse", EvaluateEscalationResponseSchema);
registry.register("BuildResultsPayloadRequest", BuildResultsPayloadRequestSchema);
Expand Down Expand Up @@ -331,6 +335,32 @@ export function buildOpenApiSpec() {
400: { description: "Invalid scoring preview input" },
},
});
registry.registerPath({
method: "post",
path: "/v1/scoring/eligibility-plan",
summary: "Derive a contributor eligibility plan from a scoring preview — REST mirror of loopover_get_eligibility_plan (#9301)",
responses: {
200: {
description:
"Structured eligibility plan over a server-built score preview — mirrors the loopover_get_eligibility_plan MCP tool. Advisory only; it explains eligibility, it does not open issues or PRs",
content: { "application/json": { schema: EligibilityPlanResponseSchema } },
},
400: { description: "Invalid scoring preview input" },
},
});
registry.registerPath({
method: "post",
path: "/v1/scoring/explain-breakdown",
summary: "Explain a score breakdown from a scoring preview — REST mirror of loopover_explain_score_breakdown (#9301)",
responses: {
200: {
description:
"Score multiplier breakdown and gate highlights over a server-built score preview — mirrors the loopover_explain_score_breakdown MCP tool. Requires contributorLogin in the request body",
content: { "application/json": { schema: ScoreBreakdownResponseSchema } },
},
400: { description: "Invalid scoring preview input or missing contributorLogin" },
},
});
registry.registerPath({
method: "get",
path: "/v1/sync/status",
Expand Down
37 changes: 37 additions & 0 deletions test/unit/openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
intakeIdeaShape,
intakeIdeaOutputSchema,
planIdeaClaimsOutputSchema,
eligibilityPlanOutputSchema,
scoreBreakdownOutputSchema,
} from "../../src/mcp/server";

describe("OpenAPI contract", () => {
Expand Down Expand Up @@ -49,6 +51,8 @@ describe("OpenAPI contract", () => {
expect(spec.paths["/v1/agent/explain-blockers"]).toBeDefined();
expect(spec.paths["/v1/scoring/model"]).toBeDefined();
expect(spec.paths["/v1/scoring/preview"]).toBeDefined();
expect(spec.paths["/v1/scoring/eligibility-plan"]).toBeDefined();
expect(spec.paths["/v1/scoring/explain-breakdown"]).toBeDefined();
expect(spec.paths["/v1/upstream/status"]).toBeDefined();
expect(spec.paths["/v1/upstream/ruleset"]).toBeDefined();
expect(spec.paths["/v1/upstream/drift"]).toBeDefined();
Expand Down Expand Up @@ -226,6 +230,39 @@ describe("OpenAPI contract", () => {
expect(spec.paths["/v1/loop/request-apr-transfer"]).toBeUndefined();
});

// #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.
it("documents the /v1/scoring/eligibility-plan and /v1/scoring/explain-breakdown routes with tool-parity schemas (#9301)", () => {
const spec = buildOpenApiSpec();
const schemas = spec.components?.schemas ?? {};

const propKeys = (name: string) =>
Object.keys((schemas[name] as { properties?: Record<string, unknown> }).properties ?? {}).sort();

const cases = [
{
path: "/v1/scoring/eligibility-plan",
response: "EligibilityPlanResponse",
outputShape: eligibilityPlanOutputSchema,
},
{
path: "/v1/scoring/explain-breakdown",
response: "ScoreBreakdownResponse",
outputShape: scoreBreakdownOutputSchema,
},
];

for (const { path, response, outputShape } of cases) {
const op = spec.paths[path]?.post;
expect(op, `${path} should be a documented POST path`).toBeDefined();
expect(op?.responses?.["200"], `${path} should document a 200 response`).toBeDefined();

expect(schemas[response], `${response} component should be registered`).toBeDefined();
expect(propKeys(response)).toEqual(Object.keys(outputShape).sort());
}
});

it("declares an `in: path` parameter for every {templated} path segment (Cloudflare schema-validation warning 30046)", () => {
const spec = buildOpenApiSpec();
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
Expand Down
Loading