Separate local parsing from model indexing, bind review decisions to immutable manifests, persist vectors behind active profiles, and expose retrieval, chat, evaluation, and document workflows through the React workbench. Constraint: Live Bailian authentication currently fails for all three configured capabilities Rejected: Direct upload-to-embedding flow | bypasses local review and manifest binding Confidence: high Scope-risk: broad Directive: Keep private-data deployment blocked until authentication, RBAC, and separate database roles land Tested: make verify; fresh and replay Docker document smoke; worker recovery smoke; frozen synthetic evaluation; migration 0003-0004 roundtrip Not-tested: Successful live Bailian calls, OCR, real multi-user authorization
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { loadOpenApiSchema, renderOpenApiTypes } from "./openapi-contract.mjs";
|
||||
|
||||
const outputPath = resolve("src/api/schema.generated.ts");
|
||||
const [expected, actual] = await Promise.all([
|
||||
loadOpenApiSchema().then(renderOpenApiTypes),
|
||||
readFile(outputPath, "utf8"),
|
||||
]);
|
||||
|
||||
if (actual !== expected) {
|
||||
throw new Error("Generated OpenAPI types are stale; run npm run generate:api");
|
||||
}
|
||||
|
||||
console.log("Generated OpenAPI types match the offline FastAPI contract");
|
||||
@@ -1,41 +1,10 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import openapiTS, { astToString, COMMENT_HEADER } from "openapi-typescript";
|
||||
import { loadOpenApiSchema, renderOpenApiTypes } from "./openapi-contract.mjs";
|
||||
|
||||
const schemaUrl = process.env.OPENAPI_SCHEMA_URL ?? "http://127.0.0.1:8000/openapi.json";
|
||||
const outputPath = resolve("src/api/schema.generated.ts");
|
||||
const requiredPaths = ["/api/v1/demo/status", "/api/v1/demo/search"];
|
||||
const schema = await loadOpenApiSchema();
|
||||
await writeFile(outputPath, await renderOpenApiTypes(schema), "utf8");
|
||||
|
||||
const response = await fetch(schemaUrl, {
|
||||
headers: { Accept: "application/json" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAPI schema request failed with HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const schema = await response.json();
|
||||
if (typeof schema !== "object" || schema === null || !("paths" in schema)) {
|
||||
throw new Error("OpenAPI response does not contain a paths object");
|
||||
}
|
||||
|
||||
const paths = schema.paths;
|
||||
if (typeof paths !== "object" || paths === null) {
|
||||
throw new Error("OpenAPI paths value is invalid");
|
||||
}
|
||||
|
||||
for (const path of requiredPaths) {
|
||||
if (!(path in paths)) {
|
||||
throw new Error(`Required API path is missing: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
const ast = await openapiTS(schema, {
|
||||
alphabetize: true,
|
||||
immutable: true,
|
||||
});
|
||||
await writeFile(outputPath, `${COMMENT_HEADER}${astToString(ast)}`, "utf8");
|
||||
|
||||
console.log(`Generated ${outputPath} from ${schemaUrl}`);
|
||||
console.log(`Generated ${outputPath} from the offline FastAPI application contract`);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import openapiTS, { astToString, COMMENT_HEADER } from "openapi-typescript";
|
||||
|
||||
const requiredPaths = [
|
||||
"/api/v1/demo/status",
|
||||
"/api/v1/demo/search",
|
||||
"/api/v1/retrieval/search",
|
||||
"/api/v1/chat/completions",
|
||||
"/api/v1/document-uploads",
|
||||
"/api/v1/document-uploads/{upload_id}/content",
|
||||
"/api/v1/document-uploads/{upload_id}/complete",
|
||||
"/api/v1/documents",
|
||||
"/api/v1/documents/{document_id}",
|
||||
"/api/v1/documents/{document_id}/review-bundle",
|
||||
"/api/v1/documents/{document_id}/review-decisions",
|
||||
"/api/v1/document-jobs/{job_id}",
|
||||
];
|
||||
|
||||
async function schemaFromUrl(schemaUrl) {
|
||||
const response = await fetch(schemaUrl, {
|
||||
headers: { Accept: "application/json" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`OpenAPI schema request failed with HTTP ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function offlineSchema() {
|
||||
const backendDirectory = resolve("..", "backend");
|
||||
const python = process.env.BACKEND_PYTHON ?? resolve(backendDirectory, ".venv/bin/python");
|
||||
const result = spawnSync(python, ["-m", "app.tools.export_openapi"], {
|
||||
cwd: backendDirectory,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0 || result.error || !result.stdout) {
|
||||
throw new Error("Offline OpenAPI export failed; run make backend-sync first");
|
||||
}
|
||||
try {
|
||||
return JSON.parse(result.stdout);
|
||||
} catch {
|
||||
throw new Error("Offline OpenAPI export returned invalid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadOpenApiSchema() {
|
||||
const schema = process.env.OPENAPI_SCHEMA_URL
|
||||
? await schemaFromUrl(process.env.OPENAPI_SCHEMA_URL)
|
||||
: offlineSchema();
|
||||
if (typeof schema !== "object" || schema === null || !("paths" in schema)) {
|
||||
throw new Error("OpenAPI response does not contain a paths object");
|
||||
}
|
||||
const paths = schema.paths;
|
||||
if (typeof paths !== "object" || paths === null) {
|
||||
throw new Error("OpenAPI paths value is invalid");
|
||||
}
|
||||
for (const path of requiredPaths) {
|
||||
if (!(path in paths)) throw new Error(`Required API path is missing: ${path}`);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
export async function renderOpenApiTypes(schema) {
|
||||
const ast = await openapiTS(schema, { alphabetize: true, immutable: true });
|
||||
return `${COMMENT_HEADER}${astToString(ast)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user