⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
buildOpenApiSpec() registers the /v1/internal/jobs/* family twice, from two places, with nothing detecting the collision:
src/openapi/spec.ts:2318-2397 — five explicit registry.registerPath blocks plus a nine-entry loop at :2376-2397, six of whose paths are registered again below.
jobRoutes() in src/openapi/internal-and-public-route-specs.ts:65-110, called from registerInternalAndPublicRouteSpecs at src/openapi/spec.ts:2415.
11 POST operations are therefore registered twice. @asteasolutions/zod-to-openapi keeps the last one, so the spec.ts versions are silently thrown away. Verified against the committed apps/loopover-ui/public/openapi.json: POST /v1/internal/jobs/backfill-pr-details publishes operationId: "queueBackfillPrDetailsJob" with responses ["202","401"] — the spec.ts block at :2352 declaring postInternalJobsBackfillPrDetails and a 400 is gone from the document entirely. The route does return 400 (src/api/routes.ts:5055).
Three loop entries are NOT duplicated and stay in spec.ts — build-contributor-evidence, build-burden-forecasts, repair-data-fidelity. Because they are legacy registerPath calls with no declared auth, and requiresApiToken returns false for /v1/internal/* (src/auth/route-auth.ts:79 — that family is gated by its own middleware at src/api/routes.ts:1216-1220), applySecurityMetadata leaves them with no security key at all. They publish a 401 response and no scheme that could produce it.
On top of the duplication, the surviving jobRoutes() table publishes statuses the handlers do not return:
SINGLE_JOBS (internal-and-public-route-specs.ts:47-50) gives rag-index and regate-pr { RAN (200), 400, 401 } and the summary "Run the job to ...". Both handlers enqueue and answer 202 (src/api/routes.ts:4964, :4995), and both can answer 404 (:4952, :4986). The published 200 is unreachable for both, and rag-index never returns 400 at all (:4953 swallows an unparseable body into {}).
QUEUED/RAN (:26-27) declare no 400, but nine job routes validate their body and return one: backfill-repo-segment (routes.ts:5016, :5018) and /run (:5037, :5039), backfill-pr-details (:5055) and /run (:5072), build-contributor-decision-packs/run (:5128), refresh-contributor-activity (:5134) and /run (:5143), generate-review-recap (:5188) and /run (:5229).
Requirements
- Delete the duplicate
/v1/internal/jobs/* registrations from src/openapi/spec.ts (the five explicit blocks at :2318-2375 and the whole loop at :2376-2397). POST /v1/internal/bounties/import at :2398-2408 is NOT a job route and must be left alone.
- Add
build-contributor-evidence, build-burden-forecasts, and repair-data-fidelity to SINGLE_JOBS so all three keep an operation, now with auth: "internal" and therefore a real LoopOverBearer security stanza. Their handlers (src/api/routes.ts:5110, :5148, :5195) all return 202 and never 400.
SINGLE_JOBS entries must publish 202 as the success status (not 200), because every one of them enqueues. Correct their summaries accordingly ("Queue a job to ...", not "Run the job to ...").
rag-index and regate-pr must additionally declare 404. rag-index must NOT declare 400; regate-pr must.
- The nine routes listed in Context that validate their body must declare
400 in addition to their existing statuses. Add per-entry response overrides; do not widen the shared QUEUED/RAN constants, which would wrongly attach a 400 to the routes that accept any body.
- Add a duplicate-registration guard: a test that asserts no
method + path pair is contributed to the registry more than once by buildOpenApiSpec().
- Regenerate and commit
apps/loopover-ui/public/openapi.json.
⚠️ Required pattern: every job operation must be declared through registerRouteSpec from the tables in src/openapi/internal-and-public-route-specs.ts (the JOB_PAIRS / SINGLE_JOBS / RUN_ONLY_JOBS shape already there), exactly as #9531 established. It does NOT satisfy this issue to keep the spec.ts blocks and merely make them agree with the table; to delete the spec.ts blocks without carrying their 400s across; to add a new third registration site; or to "fix" the duplication by making registerRouteSpec overwrite-tolerant instead of removing the duplicate call sites.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example deleting the duplicate spec.ts blocks without adding the duplicate-registration guard test, or correcting the SINGLE_JOBS success status without declaring the nine missing 400s — does not resolve this issue.
Test Coverage Requirements
src/openapi/** is inside Codecov's src/** include; the 99% branch-counted patch gate applies. The spec modules are data tables consumed by registerRouteSpec, so coverage comes from the assertions above running buildOpenApiSpec(). Both arms of any conditional added for a per-entry response override must be exercised (an entry that overrides and one that does not). The duplicate-registration guard is the named regression test for this fix.
Expected Outcome
Each /v1/internal/jobs/* operation is registered exactly once, from one table; the published success status matches what the handler returns; every body-validating job route documents its 400; every /v1/internal/* operation declares the bearer it actually requires; and a future second registration of the same path fails CI instead of silently discarding one of the two.
Links & Resources
src/openapi/spec.ts:2318-2408, :2414-2415; src/openapi/internal-and-public-route-specs.ts:25-110; handlers at src/api/routes.ts:4951-5000, :5015-5240, :5110, :5148, :5195; src/auth/route-auth.ts:79; test/unit/route-spec-ratchet.test.ts. Related closed work: #9519, #9531.
Context
buildOpenApiSpec()registers the/v1/internal/jobs/*family twice, from two places, with nothing detecting the collision:src/openapi/spec.ts:2318-2397— five explicitregistry.registerPathblocks plus a nine-entry loop at:2376-2397, six of whose paths are registered again below.jobRoutes()insrc/openapi/internal-and-public-route-specs.ts:65-110, called fromregisterInternalAndPublicRouteSpecsatsrc/openapi/spec.ts:2415.11 POST operations are therefore registered twice.
@asteasolutions/zod-to-openapikeeps the last one, so thespec.tsversions are silently thrown away. Verified against the committedapps/loopover-ui/public/openapi.json:POST /v1/internal/jobs/backfill-pr-detailspublishesoperationId: "queueBackfillPrDetailsJob"with responses["202","401"]— thespec.tsblock at:2352declaringpostInternalJobsBackfillPrDetailsand a 400 is gone from the document entirely. The route does return 400 (src/api/routes.ts:5055).Three loop entries are NOT duplicated and stay in
spec.ts—build-contributor-evidence,build-burden-forecasts,repair-data-fidelity. Because they are legacyregisterPathcalls with no declared auth, andrequiresApiTokenreturns false for/v1/internal/*(src/auth/route-auth.ts:79— that family is gated by its own middleware atsrc/api/routes.ts:1216-1220),applySecurityMetadataleaves them with nosecuritykey at all. They publish a401response and no scheme that could produce it.On top of the duplication, the surviving
jobRoutes()table publishes statuses the handlers do not return:SINGLE_JOBS(internal-and-public-route-specs.ts:47-50) givesrag-indexandregate-pr{ RAN (200), 400, 401 }and the summary "Run the job to ...". Both handlers enqueue and answer 202 (src/api/routes.ts:4964,:4995), and both can answer 404 (:4952,:4986). The published 200 is unreachable for both, andrag-indexnever returns 400 at all (:4953swallows an unparseable body into{}).QUEUED/RAN(:26-27) declare no 400, but nine job routes validate their body and return one:backfill-repo-segment(routes.ts:5016,:5018) and/run(:5037,:5039),backfill-pr-details(:5055) and/run(:5072),build-contributor-decision-packs/run(:5128),refresh-contributor-activity(:5134) and/run(:5143),generate-review-recap(:5188) and/run(:5229).Requirements
/v1/internal/jobs/*registrations fromsrc/openapi/spec.ts(the five explicit blocks at:2318-2375and the whole loop at:2376-2397).POST /v1/internal/bounties/importat:2398-2408is NOT a job route and must be left alone.build-contributor-evidence,build-burden-forecasts, andrepair-data-fidelitytoSINGLE_JOBSso all three keep an operation, now withauth: "internal"and therefore a realLoopOverBearersecurity stanza. Their handlers (src/api/routes.ts:5110,:5148,:5195) all return 202 and never 400.SINGLE_JOBSentries must publish202as the success status (not 200), because every one of them enqueues. Correct their summaries accordingly ("Queue a job to ...", not "Run the job to ...").rag-indexandregate-prmust additionally declare404.rag-indexmust NOT declare400;regate-prmust.400in addition to their existing statuses. Add per-entry response overrides; do not widen the sharedQUEUED/RANconstants, which would wrongly attach a 400 to the routes that accept any body.method + pathpair is contributed to the registry more than once bybuildOpenApiSpec().apps/loopover-ui/public/openapi.json.Deliverables
src/openapi/spec.tsno longer registers any/v1/internal/jobs/*path;grep -c 'internal/jobs' src/openapi/spec.tsreturns 0.SINGLE_JOBScontainsrag-index,regate-pr,build-contributor-evidence,build-burden-forecasts, andrepair-data-fidelity, each with a202success response andauth: "internal".rag-indexandregate-preach declare404;rag-indexdeclares no400;regate-prdeclares400.400in the generated document.test/unit/route-spec-ratchet.test.tsasserts that nomethod + pathpair is registered twice by the spec builder. This test must fail on currentmain./v1/internal/inbuildOpenApiSpec()hassecurityequal to[{ LoopOverBearer: [] }]— currently false for the three loop entries above.apps/loopover-ui/public/openapi.jsonregenerated and committed; the existingtest/unit/route-spec-ratchet.test.tsassertions still pass.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example deleting the duplicate
spec.tsblocks without adding the duplicate-registration guard test, or correcting theSINGLE_JOBSsuccess status without declaring the nine missing 400s — does not resolve this issue.Test Coverage Requirements
src/openapi/**is inside Codecov'ssrc/**include; the 99% branch-counted patch gate applies. The spec modules are data tables consumed byregisterRouteSpec, so coverage comes from the assertions above runningbuildOpenApiSpec(). Both arms of any conditional added for a per-entry response override must be exercised (an entry that overrides and one that does not). The duplicate-registration guard is the named regression test for this fix.Expected Outcome
Each
/v1/internal/jobs/*operation is registered exactly once, from one table; the published success status matches what the handler returns; every body-validating job route documents its 400; every/v1/internal/*operation declares the bearer it actually requires; and a future second registration of the same path fails CI instead of silently discarding one of the two.Links & Resources
src/openapi/spec.ts:2318-2408,:2414-2415;src/openapi/internal-and-public-route-specs.ts:25-110; handlers atsrc/api/routes.ts:4951-5000,:5015-5240,:5110,:5148,:5195;src/auth/route-auth.ts:79;test/unit/route-spec-ratchet.test.ts. Related closed work: #9519, #9531.