From 64590eff15b8f57ae5945a9ea5a59af4220fe02f Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Tue, 28 Jul 2026 03:30:41 +0800 Subject: [PATCH] docs(openapi): document contributor watches GET/POST/DELETE routes /v1/contributors/:login/watches is live across all three verbs (GET=list, POST=watch, DELETE=unwatch) as the REST mirror of the loopover_watch_issues MCP tool, but no verb appeared in the OpenAPI spec. Add ContributorWatchesResponseSchema (field-level parity with watchIssuesOutputSchema) + ContributorWatchRequestSchema (the POST/DELETE body), register them, and registerPath all three verbs mirroring the neighboring contributor notifications routes. Regenerate the committed apps/loopover-ui/public/openapi.json. --- apps/loopover-ui/public/openapi.json | 171 +++++++++++++++++++++++++++ src/openapi/schemas.ts | 23 ++++ src/openapi/spec.ts | 52 ++++++++ 3 files changed, 246 insertions(+) diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json index 6043b11b3a..c42e7bdca9 100644 --- a/apps/loopover-ui/public/openapi.json +++ b/apps/loopover-ui/public/openapi.json @@ -16290,6 +16290,52 @@ "nullable": true } } + }, + "ContributorWatchesResponse": { + "type": "object", + "properties": { + "watching": { + "type": "array", + "items": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "repoFullName", + "labels" + ] + } + }, + "changed": { + "type": "string" + } + } + }, + "ContributorWatchRequest": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "repoFullName" + ] } }, "parameters": {}, @@ -22172,6 +22218,131 @@ } ] } + }, + "/v1/contributors/{login}/watches": { + "get": { + "summary": "List a contributor's own issue-watch subscriptions — REST mirror of loopover_watch_issues (GET=list) (#9306)", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The contributor's own watch subscriptions (self-scoped): each repoFullName with its watched labels.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContributorWatchesResponse" + } + } + } + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + }, + "post": { + "summary": "Watch an issue label set for a contributor — REST mirror of loopover_watch_issues (POST=watch) (#9306)", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContributorWatchRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The updated watch subscription list after adding the repo/label watch, with a changed marker.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContributorWatchesResponse" + } + } + } + }, + "400": { + "description": "Invalid watch subscription body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + }, + "delete": { + "summary": "Unwatch a contributor's issue subscription — REST mirror of loopover_watch_issues (DELETE=unwatch) (#9306)", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContributorWatchRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The updated watch subscription list after removing the repo watch, with a changed marker.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContributorWatchesResponse" + } + } + } + }, + "400": { + "description": "Invalid watch subscription body" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } } }, "servers": [ diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 5c11c616b8..4e9320e5f6 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -539,6 +539,29 @@ export const NotificationsMarkedSchema = z }) .openapi("NotificationsMarked"); +/** + * Response for /v1/contributors/{login}/watches (GET list + POST watch + DELETE unwatch). Field-level parity + * with `watchIssuesOutputSchema` (the `loopover_watch_issues` MCP tool `outputSchema`) in src/mcp/server.ts — + * #9306. GET returns just `watching`; a POST/DELETE mutation also echoes a `changed` marker. + */ +export const ContributorWatchesResponseSchema = z + .object({ + watching: z.array(z.object({ repoFullName: z.string(), labels: z.array(z.string()) })).optional(), + changed: z.string().optional(), + }) + .openapi("ContributorWatchesResponse"); + +/** + * Request body for POST/DELETE /v1/contributors/{login}/watches. Mirrors `watchSubscriptionBodySchema` + * (src/api/routes.ts) — repoFullName plus POST-only labels; a DELETE ignores labels. #9306. + */ +export const ContributorWatchRequestSchema = z + .object({ + repoFullName: z.string(), + labels: z.array(z.string()).optional(), + }) + .openapi("ContributorWatchRequest"); + export const ContributorOpportunitySchema = z .object({ repoFullName: z.string(), diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index 7e2ce4d3d0..f567e71b25 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -37,6 +37,8 @@ import { IssueQualityReportSchema, IssueQualityResponseSchema, GateConfigEffectiveResponseSchema, + ContributorWatchesResponseSchema, + ContributorWatchRequestSchema, EligibilityPlanResponseSchema, ScoreBreakdownResponseSchema, EvaluateEscalationRequestSchema, @@ -209,6 +211,8 @@ export function buildOpenApiSpec() { registry.register("IssueQualityReport", IssueQualityReportSchema); registry.register("IssueQualityResponse", IssueQualityResponseSchema); registry.register("GateConfigEffectiveResponse", GateConfigEffectiveResponseSchema); + registry.register("ContributorWatchesResponse", ContributorWatchesResponseSchema); + registry.register("ContributorWatchRequest", ContributorWatchRequestSchema); registry.register("SelftuneOverrideAuditResponse", SelftuneOverrideAuditResponseSchema); registry.register("ClearSelftuneOverrideResponse", ClearSelftuneOverrideResponseSchema); registry.register("EligibilityPlanResponse", EligibilityPlanResponseSchema); @@ -1379,6 +1383,54 @@ export function buildOpenApiSpec() { 400: { description: "Invalid mark-read body" }, }, }); + registry.registerPath({ + method: "get", + path: "/v1/contributors/{login}/watches", + summary: "List a contributor's own issue-watch subscriptions — REST mirror of loopover_watch_issues (GET=list) (#9306)", + request: { params: z.object({ login: z.string() }) }, + responses: { + 200: { + description: "The contributor's own watch subscriptions (self-scoped): each repoFullName with its watched labels.", + content: { "application/json": { schema: ContributorWatchesResponseSchema } }, + }, + }, + }); + registry.registerPath({ + method: "post", + path: "/v1/contributors/{login}/watches", + summary: "Watch an issue label set for a contributor — REST mirror of loopover_watch_issues (POST=watch) (#9306)", + request: { + params: z.object({ login: z.string() }), + body: { + content: { "application/json": { schema: ContributorWatchRequestSchema } }, + }, + }, + responses: { + 200: { + description: "The updated watch subscription list after adding the repo/label watch, with a changed marker.", + content: { "application/json": { schema: ContributorWatchesResponseSchema } }, + }, + 400: { description: "Invalid watch subscription body" }, + }, + }); + registry.registerPath({ + method: "delete", + path: "/v1/contributors/{login}/watches", + summary: "Unwatch a contributor's issue subscription — REST mirror of loopover_watch_issues (DELETE=unwatch) (#9306)", + request: { + params: z.object({ login: z.string() }), + body: { + content: { "application/json": { schema: ContributorWatchRequestSchema } }, + }, + }, + responses: { + 200: { + description: "The updated watch subscription list after removing the repo watch, with a changed marker.", + content: { "application/json": { schema: ContributorWatchesResponseSchema } }, + }, + 400: { description: "Invalid watch subscription body" }, + }, + }); registry.registerPath({ method: "get", path: "/v1/contributors/{login}/repos/{owner}/{repo}/decision",