From 69c5cca91300a23db1bd20870f2c2387aaf06622 Mon Sep 17 00:00:00 2001 From: Tom Tang <4220945+shiba4life@users.noreply.github.com> Date: Fri, 1 May 2026 08:47:00 -0700 Subject: [PATCH 1/3] fix(api): align process_json response type across Rust handler, OpenAPI spec, and TS client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `POST /api/ingestion/process` endpoint had three different response shapes: - The Rust handler returned `ProcessJsonResponse { success, progress_id, message }` (defined via the `handler_response!` macro at `src/handlers/ingestion.rs:25`). - The route's `#[utoipa::path]` annotation declared `body = IngestionResponse` — an unrelated 6-field struct (`mutations_executed`, `new_schema_created`, etc.) that this endpoint never returns. - The TS client invented a third shape `ProcessIngestionResponse` with `records_processed`, `mutations_executed`, `schema_created`, `ai_analysis` — none of which existed on the wire. This is Phase 1 of the API typegen unification effort (see gbrain `projects/api-typegen-unification`). One audit finding fixed; seven remain. Changes: - `src/handlers/ingestion.rs`: declare `ProcessJsonResponse` manually instead of via `handler_response!` so the `utoipa::ToSchema` derive can be added. Phase 2 will fold `ToSchema` into the macro itself for all responses. - `src/server/routes/ingestion.rs`: change the utoipa annotation from `body = IngestionResponse` to `body = ProcessJsonResponse`, fix the status code to 202 Accepted (matches the actual handler), and import the type so the macro can resolve the bare identifier. - `src/server/openapi.rs`: register `ProcessJsonResponse` so it appears in the generated component schemas. - `src/server/static-react/src/types/openapi.ts`: add the schema definition and wire the `process_json` operation to the new response. (Pre-existing `Query`-ref drift on `main` blocks `npm run generate:api`, so this is a surgical hand edit mirroring what the regenerator would produce.) - `src/server/static-react/src/api/clients/ingestionClient.ts`: rename `ProcessIngestionResponse` → `ProcessJsonResponse` and replace its fictional fields with the real `{ success, progress_id, message }` shape. UI callers read these via `response.data` and pass them through to `IngestionReport`, which already only renders when `schemas_written` is non-empty — so the type-truth fix doesn't change runtime behavior; it just stops lying to consumers about fields that were always `undefined`. Verification: - `cargo fmt --all -- --check` - `cargo check --workspace` - `cargo nextest run --workspace --lib` — 859 passed - `cd src/server/static-react && npm test` — 650 passed, 6 skipped - `npm run lint` — 3 errors, all pre-existing on `main` (HTMLCanvasElement / HashChangeEvent globals), none introduced by this change References: src/handlers/ingestion.rs:25-32, src/server/routes/ingestion.rs:99, gbrain projects/api-typegen-unification (2026-05-01 audit). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/handlers/ingestion.rs | 22 ++++++++++----- src/server/openapi.rs | 1 + src/server/routes/ingestion.rs | 13 +++++++-- .../src/api/clients/ingestionClient.ts | 28 ++++++++++--------- src/server/static-react/src/types/openapi.ts | 12 ++++++-- 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/handlers/ingestion.rs b/src/handlers/ingestion.rs index ee501efc..5357fa2c 100644 --- a/src/handlers/ingestion.rs +++ b/src/handlers/ingestion.rs @@ -22,13 +22,21 @@ use tracing::Instrument; /// with Lambda handlers in exemem-infra. pub type ProcessJsonRequest = IngestionRequest; -handler_response! { - /// Response for process_json (immediate response) - pub struct ProcessJsonResponse { - pub success: bool, - pub progress_id: String, - pub message: String, - } +/// Response for process_json (immediate response). +/// +/// Manually declared (instead of via `handler_response!`) so the `utoipa::ToSchema` +/// derive can be added for the OpenAPI registry. Phase 2 of the API typegen +/// unification project will fold `ToSchema` into the macro itself. +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))] +#[cfg_attr( + feature = "ts-bindings", + ts(export, export_to = "src/fold_node/static-react/src/types/") +)] +pub struct ProcessJsonResponse { + pub success: bool, + pub progress_id: String, + pub message: String, } /// Response type for get_all_progress diff --git a/src/server/openapi.rs b/src/server/openapi.rs index bc1abcce..723ddc8a 100644 --- a/src/server/openapi.rs +++ b/src/server/openapi.rs @@ -54,6 +54,7 @@ use utoipa::OpenApi; crate::ingestion::config::AnthropicConfig, crate::ingestion::IngestionRequest, crate::ingestion::IngestionResponse, + crate::handlers::ingestion::ProcessJsonResponse, crate::server::routes::log::LogLevelUpdate, crate::server::routes::admin::ResetDatabaseRequest, crate::server::routes::admin::AdminJobResponse, diff --git a/src/server/routes/ingestion.rs b/src/server/routes/ingestion.rs index 794214c6..38cce9a7 100644 --- a/src/server/routes/ingestion.rs +++ b/src/server/routes/ingestion.rs @@ -3,6 +3,11 @@ //! All actix-web glue for ingestion lives here. The pure pipeline logic is //! parameterized and lives in `crate::ingestion`. +// Imported so the bare `ProcessJsonResponse` token in the `#[utoipa::path]` +// `body =` annotation below resolves; suppress the unused-import lint because +// the macro consumes it without producing a value-level reference. +#[allow(unused_imports)] +use crate::handlers::ingestion::ProcessJsonResponse; use crate::ingestion::config::{AIProvider, OllamaGenerationParams}; use crate::ingestion::helpers::{ fetch_ollama_models, resolve_folder_path, spawn_file_ingestion_tasks, start_file_progress, @@ -90,13 +95,17 @@ pub(crate) use ingestion_context_or_return; // ── Core ingestion routes ────────────────────────────────────────── -/// Process JSON ingestion request +/// Process JSON ingestion request. +/// +/// Returns 202 Accepted immediately — the body is a job-started envelope, not the +/// final ingestion result. Clients poll `/ingestion/progress/{progress_id}` for +/// the actual outcome. #[utoipa::path( post, path = "/api/ingestion/process", tag = "ingestion", request_body = IngestionRequest, - responses((status = 200, description = "Ingestion response", body = IngestionResponse)) + responses((status = 202, description = "Ingestion job started", body = ProcessJsonResponse)) )] pub async fn process_json( request: web::Json, diff --git a/src/server/static-react/src/api/clients/ingestionClient.ts b/src/server/static-react/src/api/clients/ingestionClient.ts index f2a62d35..3920a2d0 100644 --- a/src/server/static-react/src/api/clients/ingestionClient.ts +++ b/src/server/static-react/src/api/clients/ingestionClient.ts @@ -141,18 +141,20 @@ export interface ProcessIngestionRequest { // ... -export interface ProcessIngestionResponse { +/** + * Body returned by `POST /api/ingestion/process` (HTTP 202 Accepted). + * + * The endpoint kicks off a background ingestion job and returns immediately — + * the actual results (schemas written, mutations executed, etc.) live on + * `/ingestion/progress/{progress_id}`. This shape mirrors the Rust + * `ProcessJsonResponse` exactly; the previously declared fields + * (`records_processed`, `mutations_executed`, `schema_created`, + * `ai_analysis`) never existed on the wire. + */ +export interface ProcessJsonResponse { success: boolean; - error?: string; - schema_created?: string; - records_processed?: number; - mutations_executed?: number; - ai_analysis?: { - schema_recommendations?: string[]; - data_quality_notes?: string[]; - execution_summary?: string; - }; - progress_id?: string; // ID for tracking progress + progress_id: string; + message: string; } // Smart Folder types @@ -412,7 +414,7 @@ export class UnifiedIngestionClient { pubKey?: string; orgHash?: string; } = {}, - ): Promise> { + ): Promise> { // Generate a UUID for progress tracking const progressId = crypto.randomUUID(); @@ -432,7 +434,7 @@ export class UnifiedIngestionClient { ); } - return this.client.post( + return this.client.post( API_ENDPOINTS.PROCESS_JSON, request, { diff --git a/src/server/static-react/src/types/openapi.ts b/src/server/static-react/src/types/openapi.ts index 4118cc30..67f23721 100644 --- a/src/server/static-react/src/types/openapi.ts +++ b/src/server/static-react/src/types/openapi.ts @@ -607,6 +607,12 @@ export interface components { base_url: string; model: string; }; + /** @description Response for process_json (immediate response). */ + ProcessJsonResponse: { + message: string; + progress_id: string; + success: boolean; + }; /** @description Field storing a range of values. */ RangeField: { inner: components["schemas"]["FieldCommon"]; @@ -771,13 +777,13 @@ export interface operations { }; }; responses: { - /** @description Ingestion response */ - 200: { + /** @description Ingestion job started */ + 202: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IngestionResponse"]; + "application/json": components["schemas"]["ProcessJsonResponse"]; }; }; }; From 96cbac1767d3cd0ea3f1536548e851578708eb40 Mon Sep 17 00:00:00 2001 From: Tom Tang <4220945+shiba4life@users.noreply.github.com> Date: Fri, 1 May 2026 08:54:43 -0700 Subject: [PATCH 2/3] ci: include best-effort OpenAPI drift check (matches foundation PR #781) --- .github/workflows/ci-tests.yml | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index a8294354..7e0f27f7 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -107,6 +107,53 @@ jobs: - name: Run doc tests run: cargo test --workspace --doc + # OpenAPI drift check — regenerates src/types/openapi.ts from the + # current Rust source and fails if it differs from the committed + # file. Run `npm run generate:api` locally after backend route or + # schema changes (run.sh dev does this automatically). + # + # Best-effort while the underlying utoipa coverage is incomplete: + # the spec currently has unresolved $refs (missing ToSchema derives + # on OllamaGenerationParams, VisionBackend, UseCaseOverride, Role, + # and many more — tracked in gbrain projects/api-typegen-unification + # Phase 3). Until that lands, openapi-typescript@7.x errors out, so + # the drift verify step is gated on regen succeeding. Once Phase 3 + # is in, drop the `continue-on-error` and `if:` and require it to + # pass. + - name: Setup Node.js (for OpenAPI drift check) + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: src/server/static-react/package-lock.json + + - name: Install frontend dependencies + working-directory: src/server/static-react + run: npm ci + + - name: Dump current OpenAPI spec + run: cargo run --quiet --bin openapi_dump > target/openapi.json + + - name: Regenerate OpenAPI TypeScript types + id: regen_openapi_ts + working-directory: src/server/static-react + run: npm run generate:api + continue-on-error: true + + - name: Note regen failure (informational, until Phase 3 lands) + if: steps.regen_openapi_ts.outcome != 'success' + run: | + echo "::warning::npm run generate:api errored — likely unresolved \$refs in target/openapi.json. Tracked under gbrain projects/api-typegen-unification Phase 3 (utoipa coverage sweep). Drift verification skipped this run." + + - name: Verify openapi.ts is up to date (when regen succeeded) + if: steps.regen_openapi_ts.outcome == 'success' + run: | + if ! git diff --exit-code src/server/static-react/src/types/openapi.ts; then + echo "::error::src/server/static-react/src/types/openapi.ts is stale." + echo "Run 'cargo run --bin openapi_dump > target/openapi.json && (cd src/server/static-react && npm run generate:api)' and commit the result." + exit 1 + fi + frontend-tests: name: Frontend Tests runs-on: ubuntu-latest @@ -138,6 +185,11 @@ jobs: working-directory: src/server/static-react run: npm ci + - name: Typecheck (tsc --noEmit) + if: steps.check-frontend.outputs.exists == 'true' + working-directory: src/server/static-react + run: npm run typecheck + - name: Run npm test if: steps.check-frontend.outputs.exists == 'true' working-directory: src/server/static-react From cb89e0139fd6f37d563bb16abf3ed52d0e0b1a1f Mon Sep 17 00:00:00 2001 From: Tom Tang <4220945+shiba4life@users.noreply.github.com> Date: Fri, 1 May 2026 11:36:37 -0700 Subject: [PATCH 3/3] ci: drop typecheck step (foundation PR adds the script; this branch doesn't have it yet) --- .github/workflows/ci-tests.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 7e0f27f7..2f6ff0b8 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -185,11 +185,6 @@ jobs: working-directory: src/server/static-react run: npm ci - - name: Typecheck (tsc --noEmit) - if: steps.check-frontend.outputs.exists == 'true' - working-directory: src/server/static-react - run: npm run typecheck - - name: Run npm test if: steps.check-frontend.outputs.exists == 'true' working-directory: src/server/static-react