# Tasks — add-export-download-handler ## 1. Error codes - [ ] Add `JOB_NOT_READY` to `EXPORT_CODES` registry in `src/error-codes/error-codes.ts` - [ ] Update the JSDoc registry summary to describe `JOB_NOT_READY` semantics (raised by download handler when job is not in `completed` state) - [ ] Update `src/error-codes/error-codes.test.ts` to assert `JOB_NOT_READY` narrows through the public type union ## 2. Problem-details renderer - [ ] Author `src/routes/problem-json/problem-json.ts` mirroring the `api-plugin-import` sibling (`PROBLEM_JSON_CONTENT_TYPE`, `ProblemDetails`, `renderProblem`, `respondProblemJson`) - [ ] Map `SuaveplanError` → `status` via `context.statusCode` (404 for `JOB_NOT_FOUND`, 409 for `JOB_NOT_READY`, 400 for everything else under 500) - [ ] Author `src/routes/problem-json/problem-json.test.ts` covering: SuaveplanError mapping, plain Error fallback, unknown throwable fallback, missing `statusCode` defaults to 500 - [ ] Author `src/routes/problem-json/problem-json.md` (≥200 words, Purpose / Features / Usage Basic / Usage Advanced / API Reference / Implementation Notes) - [ ] Add `./routes/problem-json/problem-json` subpath export to `package.json` - [ ] Re-export `renderProblem`, `respondProblemJson`, `PROBLEM_JSON_CONTENT_TYPE`, `ProblemDetails` from `src/index.ts` ## 3. OTel instruments - [ ] Extend `ExportInstruments` in `src/otel/instruments.ts` with `downloadServed: Counter` and `downloadFailed: Counter` - [ ] Register the two counters via `meter.createCounter("export.download.served", …)` and `meter.createCounter("export.download.failed", …)` - [ ] Update `src/otel/instruments.md` with the two new instrument descriptions - [ ] Extend `src/otel/instruments.test.ts` with: "emits download.served on a successful download" and "emits download.failed{reason} on a not-ready download" ## 4. Download handler - [ ] Create `src/routes/download/download.ts` exporting `runDownloadExport(deps, jobId)` returning a discriminated `DownloadOutcome` union (`{ kind: "ok"; body; contentType; filename }` / `{ kind: "not-found" }` / `{ kind: "not-ready"; status }` / `{ kind: "provider-not-found"; resource }` / `{ kind: "failed"; error }`) - [ ] Implementation flow: `jobStore.get(id)` → `null` ⇒ `not-found` raising `EXPORT.JOB_NOT_FOUND`; `status !== "completed"` ⇒ `not-ready` raising `EXPORT.JOB_NOT_READY`; `providers.get(job.resource)` → `null` ⇒ `provider-not-found` raising `EXPORT.PROVIDER_NOT_FOUND`; on any exception during fetch/render emit `EXPORT.PROVIDER_FETCH_FAILED` - [ ] Author `src/routes/download/download.md` (≥200 words) - [ ] Author `src/routes/download/download.test.ts` covering every branch with real CSV / JSON / XLSX renders ## 5. Route registration - [ ] Extend `ExportRoutesDeps` in `src/routes/exports/exports.ts` with optional `jobStore?: ExportJobStore` - [ ] Mount `GET /{id}/download` after the existing `GET /{id}` route; build the inline `Map` adapter when `jobStore` is absent - [ ] Wire the download outcome through `respondProblemJson` for every non-ok branch - [ ] OpenAPI route definition: `params: z.object({ id: z.string() })`, `responses`: `200` (binary), `404`, `409`, `502`, all carrying `application/problem+json` schema for the failure shapes - [ ] Update `src/routes/exports/exports.md` with a paragraph on the new endpoint - [ ] Extend `src/routes/exports/exports.test.ts` with an end-to-end happy-path download + a 404 + a 409 + a 502 test using a real provider + real `MemoryExportJobStore` ## 6. Plugin wiring - [ ] In `src/plugin/plugin.ts` `runOnReady`, look up `ctx.getService("exportJobStore")` and thread it into `createExportRoutes({ …, jobStore })`. Absent slot ⇒ omit; existing behaviour preserved - [ ] Extend `src/plugin/plugin.test.ts` with "download route uses exportJobStore service when wired" using a `MemoryExportJobStore` ## 7. Spec conformance - [ ] `src/api-plugin-export.spec-conformance.test.ts` already pins every `EXPORT_*` code; verify `JOB_NOT_READY` is thrown by the download handler so the pin holds - [ ] Run `bun scripts/openspec-conformance.ts @suaveplan/api-plugin-export` — expect zero errors ## 8. Pre-archive gates - [ ] `bunx biome check src/` clean - [ ] `bunx turbo lint typecheck test --filter=@suaveplan/api-plugin-export` green - [ ] `bunx turbo test:coverage --filter=@suaveplan/api-plugin-export` ≥100/100/100/100 on touched modules - [ ] `bunx turbo build --filter=@suaveplan/api-plugin-export` green - [ ] `bun run docs:check` (package-scoped, via `package.json` script) clean ## 9. Archive - [ ] Fold `specs/api-plugin-export/spec.md` delta into `openspec/specs/api-plugin-export/spec.md` - [ ] Move `openspec/changes/add-export-download-handler` → `openspec/archive/2026-05-20-add-export-download-handler` - [ ] Single commit `feat(api-plugin-export): add GET /:id/download handler (was §4-deferred)` and push (pre-pull-rebase autostash)