Skip to content

Commit 4b761fa

Browse files
authored
openapi: /v1/scoring/eligibility-plan + /v1/scoring/explain-breakdown missing from spec (MCP tools + schemas already exist) (#9421)
Fixes #9301 Co-authored-by: phamngocquy <phamngocquy@users.noreply.github.com>
1 parent 726a12e commit 4b761fa

5 files changed

Lines changed: 212 additions & 2 deletions

File tree

apps/loopover-ui/public/openapi.json

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15330,6 +15330,62 @@
1533015330
"required": [
1533115331
"ok"
1533215332
]
15333+
},
15334+
"EligibilityPlanResponse": {
15335+
"type": "object",
15336+
"properties": {
15337+
"eligible": {
15338+
"type": "boolean"
15339+
},
15340+
"linkedIssueStatus": {
15341+
"type": "string"
15342+
},
15343+
"branchEligibilityStatus": {
15344+
"type": "string"
15345+
},
15346+
"blockers": {
15347+
"type": "array",
15348+
"items": {
15349+
"type": "string"
15350+
}
15351+
},
15352+
"cleanupPaths": {
15353+
"type": "array",
15354+
"items": {
15355+
"type": "string"
15356+
}
15357+
},
15358+
"linkedIssueProjection": {
15359+
"type": "string",
15360+
"nullable": true
15361+
},
15362+
"publicSummary": {
15363+
"type": "string"
15364+
}
15365+
}
15366+
},
15367+
"ScoreBreakdownResponse": {
15368+
"type": "object",
15369+
"properties": {
15370+
"repoFullName": {
15371+
"type": "string"
15372+
},
15373+
"scoreabilityStatus": {
15374+
"type": "string"
15375+
},
15376+
"effectiveEstimatedScore": {
15377+
"type": "number"
15378+
},
15379+
"components": {
15380+
"nullable": true
15381+
},
15382+
"gateHighlights": {
15383+
"nullable": true
15384+
},
15385+
"highestLeverageLever": {
15386+
"nullable": true
15387+
}
15388+
}
1533315389
}
1533415390
},
1533515391
"parameters": {},
@@ -20167,6 +20223,62 @@
2016720223
}
2016820224
]
2016920225
}
20226+
},
20227+
"/v1/scoring/eligibility-plan": {
20228+
"post": {
20229+
"summary": "Derive a contributor eligibility plan from a scoring preview — REST mirror of loopover_get_eligibility_plan (#9301)",
20230+
"responses": {
20231+
"200": {
20232+
"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",
20233+
"content": {
20234+
"application/json": {
20235+
"schema": {
20236+
"$ref": "#/components/schemas/EligibilityPlanResponse"
20237+
}
20238+
}
20239+
}
20240+
},
20241+
"400": {
20242+
"description": "Invalid scoring preview input"
20243+
}
20244+
},
20245+
"security": [
20246+
{
20247+
"LoopOverBearer": []
20248+
},
20249+
{
20250+
"LoopOverSessionCookie": []
20251+
}
20252+
]
20253+
}
20254+
},
20255+
"/v1/scoring/explain-breakdown": {
20256+
"post": {
20257+
"summary": "Explain a score breakdown from a scoring preview — REST mirror of loopover_explain_score_breakdown (#9301)",
20258+
"responses": {
20259+
"200": {
20260+
"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",
20261+
"content": {
20262+
"application/json": {
20263+
"schema": {
20264+
"$ref": "#/components/schemas/ScoreBreakdownResponse"
20265+
}
20266+
}
20267+
}
20268+
},
20269+
"400": {
20270+
"description": "Invalid scoring preview input or missing contributorLogin"
20271+
}
20272+
},
20273+
"security": [
20274+
{
20275+
"LoopOverBearer": []
20276+
},
20277+
{
20278+
"LoopOverSessionCookie": []
20279+
}
20280+
]
20281+
}
2017020282
}
2017120283
},
2017220284
"servers": [

src/mcp/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,7 +1697,7 @@ const remediationPlanOutputSchema = {
16971697
items: z.unknown().optional(),
16981698
};
16991699

1700-
const scoreBreakdownOutputSchema = {
1700+
export const scoreBreakdownOutputSchema = {
17011701
repoFullName: z.string().optional(),
17021702
scoreabilityStatus: z.string().optional(),
17031703
effectiveEstimatedScore: z.number().optional(),
@@ -1706,7 +1706,7 @@ const scoreBreakdownOutputSchema = {
17061706
highestLeverageLever: z.unknown().optional(),
17071707
};
17081708

1709-
const eligibilityPlanOutputSchema = {
1709+
export const eligibilityPlanOutputSchema = {
17101710
eligible: z.boolean().optional(),
17111711
linkedIssueStatus: z.string().optional(),
17121712
branchEligibilityStatus: z.string().optional(),

src/openapi/schemas.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,37 @@ export const GateConfigEffectiveResponseSchema = z
19261926
})
19271927
.openapi("GateConfigEffectiveResponse");
19281928

1929+
/**
1930+
* Response body for POST /v1/scoring/eligibility-plan. Field-level parity with `eligibilityPlanOutputSchema`
1931+
* (the `loopover_get_eligibility_plan` MCP tool `outputSchema`) in src/mcp/server.ts — #9301.
1932+
*/
1933+
export const EligibilityPlanResponseSchema = z
1934+
.object({
1935+
eligible: z.boolean().optional(),
1936+
linkedIssueStatus: z.string().optional(),
1937+
branchEligibilityStatus: z.string().optional(),
1938+
blockers: z.array(z.string()).optional(),
1939+
cleanupPaths: z.array(z.string()).optional(),
1940+
linkedIssueProjection: z.string().nullable().optional(),
1941+
publicSummary: z.string().optional(),
1942+
})
1943+
.openapi("EligibilityPlanResponse");
1944+
1945+
/**
1946+
* Response body for POST /v1/scoring/explain-breakdown. Field-level parity with `scoreBreakdownOutputSchema`
1947+
* (the `loopover_explain_score_breakdown` MCP tool `outputSchema`) in src/mcp/server.ts — #9301.
1948+
*/
1949+
export const ScoreBreakdownResponseSchema = z
1950+
.object({
1951+
repoFullName: z.string().optional(),
1952+
scoreabilityStatus: z.string().optional(),
1953+
effectiveEstimatedScore: z.number().optional(),
1954+
components: z.unknown().optional(),
1955+
gateHighlights: z.unknown().optional(),
1956+
highestLeverageLever: z.unknown().optional(),
1957+
})
1958+
.openapi("ScoreBreakdownResponse");
1959+
19291960
/**
19301961
* Request body for POST /v1/loop/evaluate-escalation. Field-level parity with `evaluateEscalationShape`
19311962
* (the `loopover_evaluate_escalation` MCP tool `inputSchema`) in src/mcp/server.ts — #9309.

src/openapi/spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import {
3737
IssueQualityReportSchema,
3838
IssueQualityResponseSchema,
3939
GateConfigEffectiveResponseSchema,
40+
EligibilityPlanResponseSchema,
41+
ScoreBreakdownResponseSchema,
4042
EvaluateEscalationRequestSchema,
4143
EvaluateEscalationResponseSchema,
4244
BuildResultsPayloadRequestSchema,
@@ -174,6 +176,8 @@ export function buildOpenApiSpec() {
174176
registry.register("IssueQualityReport", IssueQualityReportSchema);
175177
registry.register("IssueQualityResponse", IssueQualityResponseSchema);
176178
registry.register("GateConfigEffectiveResponse", GateConfigEffectiveResponseSchema);
179+
registry.register("EligibilityPlanResponse", EligibilityPlanResponseSchema);
180+
registry.register("ScoreBreakdownResponse", ScoreBreakdownResponseSchema);
177181
registry.register("EvaluateEscalationRequest", EvaluateEscalationRequestSchema);
178182
registry.register("EvaluateEscalationResponse", EvaluateEscalationResponseSchema);
179183
registry.register("BuildResultsPayloadRequest", BuildResultsPayloadRequestSchema);
@@ -333,6 +337,32 @@ export function buildOpenApiSpec() {
333337
400: { description: "Invalid scoring preview input" },
334338
},
335339
});
340+
registry.registerPath({
341+
method: "post",
342+
path: "/v1/scoring/eligibility-plan",
343+
summary: "Derive a contributor eligibility plan from a scoring preview — REST mirror of loopover_get_eligibility_plan (#9301)",
344+
responses: {
345+
200: {
346+
description:
347+
"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",
348+
content: { "application/json": { schema: EligibilityPlanResponseSchema } },
349+
},
350+
400: { description: "Invalid scoring preview input" },
351+
},
352+
});
353+
registry.registerPath({
354+
method: "post",
355+
path: "/v1/scoring/explain-breakdown",
356+
summary: "Explain a score breakdown from a scoring preview — REST mirror of loopover_explain_score_breakdown (#9301)",
357+
responses: {
358+
200: {
359+
description:
360+
"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",
361+
content: { "application/json": { schema: ScoreBreakdownResponseSchema } },
362+
},
363+
400: { description: "Invalid scoring preview input or missing contributorLogin" },
364+
},
365+
});
336366
registry.registerPath({
337367
method: "get",
338368
path: "/v1/sync/status",

test/unit/openapi.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
intakeIdeaShape,
1111
intakeIdeaOutputSchema,
1212
planIdeaClaimsOutputSchema,
13+
eligibilityPlanOutputSchema,
14+
scoreBreakdownOutputSchema,
1315
} from "../../src/mcp/server";
1416

1517
describe("OpenAPI contract", () => {
@@ -50,6 +52,8 @@ describe("OpenAPI contract", () => {
5052
expect(spec.paths["/v1/agent/explain-blockers"]).toBeDefined();
5153
expect(spec.paths["/v1/scoring/model"]).toBeDefined();
5254
expect(spec.paths["/v1/scoring/preview"]).toBeDefined();
55+
expect(spec.paths["/v1/scoring/eligibility-plan"]).toBeDefined();
56+
expect(spec.paths["/v1/scoring/explain-breakdown"]).toBeDefined();
5357
expect(spec.paths["/v1/upstream/status"]).toBeDefined();
5458
expect(spec.paths["/v1/upstream/ruleset"]).toBeDefined();
5559
expect(spec.paths["/v1/upstream/drift"]).toBeDefined();
@@ -229,6 +233,39 @@ describe("OpenAPI contract", () => {
229233
expect(spec.paths["/v1/loop/request-apr-transfer"]).toBeUndefined();
230234
});
231235

236+
// #9301: the two /v1/scoring/* composer routes backed by loopover_get_eligibility_plan and
237+
// loopover_explain_score_breakdown. Assert each is a documented POST path whose 200 response
238+
// component stays field-for-field in parity with the MCP tool outputSchema.
239+
it("documents the /v1/scoring/eligibility-plan and /v1/scoring/explain-breakdown routes with tool-parity schemas (#9301)", () => {
240+
const spec = buildOpenApiSpec();
241+
const schemas = spec.components?.schemas ?? {};
242+
243+
const propKeys = (name: string) =>
244+
Object.keys((schemas[name] as { properties?: Record<string, unknown> }).properties ?? {}).sort();
245+
246+
const cases = [
247+
{
248+
path: "/v1/scoring/eligibility-plan",
249+
response: "EligibilityPlanResponse",
250+
outputShape: eligibilityPlanOutputSchema,
251+
},
252+
{
253+
path: "/v1/scoring/explain-breakdown",
254+
response: "ScoreBreakdownResponse",
255+
outputShape: scoreBreakdownOutputSchema,
256+
},
257+
];
258+
259+
for (const { path, response, outputShape } of cases) {
260+
const op = spec.paths[path]?.post;
261+
expect(op, `${path} should be a documented POST path`).toBeDefined();
262+
expect(op?.responses?.["200"], `${path} should document a 200 response`).toBeDefined();
263+
264+
expect(schemas[response], `${response} component should be registered`).toBeDefined();
265+
expect(propKeys(response)).toEqual(Object.keys(outputShape).sort());
266+
}
267+
});
268+
232269
it("declares an `in: path` parameter for every {templated} path segment (Cloudflare schema-validation warning 30046)", () => {
233270
const spec = buildOpenApiSpec();
234271
for (const [path, methods] of Object.entries(spec.paths ?? {})) {

0 commit comments

Comments
 (0)