From c18d111e40e793a5a832f84e7acf8c7516a4f5b9 Mon Sep 17 00:00:00 2001 From: Mathis <154886644+echobt@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:16:06 +0000 Subject: [PATCH] fix(ci): read the backend contract from its TypeScript server The backend replaced its Rust crates with server/ (CortexLM/backend#441). Error codes and PROBLEM_TYPE_BASE now come from server/src/core/error.ts and registered paths from the route literals under server/src/api; admin-listener paths stay out of the public set. --- README.md | 4 +- scripts/check-docs-site.mjs | 69 ++++++++++++++++----------- scripts/tests/check-docs-site.test.sh | 64 ++++++++++++++++--------- 3 files changed, 85 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index bb4c8ba..40eab21 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,9 @@ merge the docs page first, then update the backend's checkout pin in the PR that adds the code. The full check fails when: - `PROBLEM_TYPE_BASE` is not `https://docs.cortex.foundation/problems` -- an `ErrorCode` is missing its `/problems/{code}` page, or a page documents a code the API does not emit +- a code in `ERROR_CODES` (`server/src/core/error.ts`) is missing its `/problems/{code}` page, or a page documents a code the API does not emit - a `docs.json` navigation slug, group root, navbar or footer href, redirect destination, or internal link has no matching MDX page -- a documented `/v1/…` path is not registered in `crates/cortex-api/src/router.rs` +- a documented `/v1/…` path is not registered by a route module under `server/src/api` - a screenshot lacks alt text, references a missing or unsafe local file, uses an unsupported format or embeds remote media - a page lacks a title, description, or icon, repeats another page's title, or has a description over 160 characters - navigation is not `navigation.tabs` with a tab for each of Chat, Code, Bot, CLI, Design, and Security, each tab and group carrying an icon diff --git a/scripts/check-docs-site.mjs b/scripts/check-docs-site.mjs index e2c6382..4611083 100644 --- a/scripts/check-docs-site.mjs +++ b/scripts/check-docs-site.mjs @@ -7,7 +7,7 @@ * * `PROBLEM_TYPE_BASE` is wire contract: every error the API returns carries a * `type` URL under `https://docs.cortex.foundation/problems`, so each - * `ErrorCode` needs a page there, and no page may document a code the API does + * error code in `ERROR_CODES` needs a page there, and no page may document a code the API does * not emit. Documenting a `/v1/…` path the router does not register is the * same class of silent decay. * @@ -59,19 +59,17 @@ function walk(dir) { return out; } -function rustErrorCodes(source) { - const block = /pub const fn as_str\(self\)[\s\S]*?match self \{([\s\S]*?)\n \}/.exec( - source, - ); +function serverErrorCodes(source) { + const block = /export const ERROR_CODES = \{([\s\S]*?)\n\}/.exec(source); if (block === null) { - fail('could not find `ErrorCode::as_str` in crates/cortex-core/src/error.rs'); + fail('could not find `ERROR_CODES` in server/src/core/error.ts'); return []; } - return [...block[1].matchAll(/=> "([a-z_]+)"/g)].map((m) => m[1]); + return [...block[1].matchAll(/^\s+([a-z_]+):/gm)].map((m) => m[1]); } -function rustProblemBase(source) { - return /pub const PROBLEM_TYPE_BASE: &str = "([^"]+)";/.exec(source)?.[1] ?? null; +function serverProblemBase(source) { + return /export const PROBLEM_TYPE_BASE = "([^"]+)";/.exec(source)?.[1] ?? null; } function tsProblemBase(source) { @@ -262,12 +260,20 @@ function internalHrefs(text) { return out; } -function routerPaths(source) { +/** + * `routes(r)` in server/src/api registers under `/v1`; `operationalRoutes` + * (health probes, `/internal/…`) stay bare. Literals already starting with + * `/v1` are `adminRoutes` on the private listener and are not public API. + */ +function routerPaths(sources) { const registered = new Set(); - for (const m of source.matchAll(/\.route\(\s*"([^"]+)"/g)) { - const path = m[1]; - if (OPERATIONAL.has(path)) registered.add(path); - else registered.add(`/v1${path}`); + for (const source of sources) { + for (const m of source.matchAll(/\.(?:get|post|put|patch|delete)\(\s*"(\/[^"]*)"/g)) { + const path = m[1]; + if (path === '/v1' || path.startsWith('/v1/')) continue; + if (OPERATIONAL.has(path) || path.startsWith('/internal/')) registered.add(path); + else registered.add(`/v1${path}`); + } } return registered; } @@ -295,26 +301,33 @@ function documentedV1Paths(text) { return found; } -const errorRs = BACKEND === null ? '' : read('crates/cortex-core/src/error.rs', BACKEND); +const errorSrc = BACKEND === null ? '' : read('server/src/core/error.ts', BACKEND); const errorsTs = BACKEND === null ? '' : read('packages/api-types/src/errors.ts', BACKEND); -const routerRs = BACKEND === null ? '' : read('crates/cortex-api/src/router.rs', BACKEND); +const API_DIR = 'server/src/api'; +if (BACKEND !== null && !existsSync(join(BACKEND, API_DIR))) fail(`missing ${API_DIR}`); +const routerSources = + BACKEND === null + ? [] + : walk(join(BACKEND, API_DIR)) + .filter((path) => path.endsWith('.ts')) + .map((path) => readFileSync(path, 'utf8')); const docsRoot = ROOT; if (BACKEND !== null) { - const rustBase = rustProblemBase(errorRs); + const serverBase = serverProblemBase(errorSrc); const tsBase = tsProblemBase(errorsTs); - if (rustBase === null) { - fail('could not find `PROBLEM_TYPE_BASE` in crates/cortex-core/src/error.rs'); - } else if (rustBase !== EXPECTED_BASE) { + if (serverBase === null) { + fail('could not find `PROBLEM_TYPE_BASE` in server/src/core/error.ts'); + } else if (serverBase !== EXPECTED_BASE) { fail( - `PROBLEM_TYPE_BASE in error.rs is ${rustBase}; user-facing problem URIs must be ${EXPECTED_BASE}`, + `PROBLEM_TYPE_BASE in server/src/core/error.ts is ${serverBase}; user-facing problem URIs must be ${EXPECTED_BASE}`, ); } if (tsBase === null) { fail('could not find `PROBLEM_TYPE_BASE` in packages/api-types/src/errors.ts'); - } else if (tsBase !== rustBase && rustBase !== null) { + } else if (tsBase !== serverBase && serverBase !== null) { fail( - `PROBLEM_TYPE_BASE: error.rs serves ${rustBase} and api-types claims ${tsBase}`, + `PROBLEM_TYPE_BASE: server/src/core/error.ts serves ${serverBase} and api-types claims ${tsBase}`, ); } else if (tsBase !== EXPECTED_BASE) { fail( @@ -328,13 +341,13 @@ const problemPages = existsSync(problemDir) ? readdirSync(problemDir).filter((name) => name.endsWith('.mdx') && name !== 'index.mdx') : []; const pageCodes = new Set(problemPages.map((name) => name.replace(/\.mdx$/, ''))); -const codes = BACKEND === null ? [...pageCodes] : rustErrorCodes(errorRs); +const codes = BACKEND === null ? [...pageCodes] : serverErrorCodes(errorSrc); if (codes.length === 0) fail('no problem codes found'); for (const code of codes) { const rel = `problems/${code}.mdx`; if (!pageCodes.has(code)) { - fail(`ErrorCode \`${code}\` has no Mintlify page at ${rel}`); + fail(`ERROR_CODES \`${code}\` has no Mintlify page at ${rel}`); continue; } const page = read(rel); @@ -346,7 +359,7 @@ for (const code of codes) { for (const extra of pageCodes) { if (!codes.includes(extra)) { fail( - `problems/${extra}.mdx documents a code that is not in ErrorCode::as_str`, + `problems/${extra}.mdx documents a code that is not in ERROR_CODES (server/src/core/error.ts)`, ); } } @@ -372,7 +385,7 @@ for (const rel of FORBIDDEN_AUTH_PAGES) { } } -const registered = routerPaths(routerRs); +const registered = routerPaths(routerSources); const registeredNorm = new Set([...registered].map(normalizePath)); const docsFiles = walk(docsRoot).filter((path) => /\.(mdx|md|json)$/.test(path)); const titles = new Map(); @@ -397,7 +410,7 @@ for (const file of docsFiles) { const norm = normalizePath(path); if (!registeredNorm.has(norm)) { fail( - `${rel} documents \`${path}\`, which is not registered in crates/cortex-api/src/router.rs`, + `${rel} documents \`${path}\`, which is not registered in server/src/api`, ); } } diff --git a/scripts/tests/check-docs-site.test.sh b/scripts/tests/check-docs-site.test.sh index 6fded44..254cd07 100755 --- a/scripts/tests/check-docs-site.test.sh +++ b/scripts/tests/check-docs-site.test.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Holds scripts/check-docs-site.mjs. No Mintlify CLI: a throwaway tree with a -# fake ErrorCode, a fake router, and a couple of MDX files is enough to prove +# fake ERROR_CODES, fake route modules, and a couple of MDX files is enough to prove # a matching site passes and that a wrong domain, a missing page, a dead # navigation entry, an unsafe image, a leaked auth internal, or an invented /v1 path # fails. @@ -18,31 +18,36 @@ trap cleanup EXIT seed() { local dest="$1" mkdir -p \ - "$dest/crates/cortex-core/src" \ - "$dest/crates/cortex-api/src" \ + "$dest/server/src/core" \ + "$dest/server/src/api" \ "$dest/packages/api-types/src" \ "$dest/site/problems" \ "$dest/site/logo" - cat > "$dest/crates/cortex-core/src/error.rs" <<'RS' -pub const PROBLEM_TYPE_BASE: &str = "https://docs.cortex.foundation/problems"; - -impl ErrorCode { - pub const fn as_str(self) -> &'static str { - match self { - Self::NotFound => "not_found", - Self::Internal => "internal", - } - } -} -RS + cat > "$dest/server/src/core/error.ts" <<'TS' +export const PROBLEM_TYPE_BASE = "https://docs.cortex.foundation/problems"; + +export const ERROR_CODES = { + not_found: [404, "Not found"], + internal: [500, "Something went wrong on our side"], +} as const satisfies Record; +TS cat > "$dest/packages/api-types/src/errors.ts" <<'TS' export const PROBLEM_TYPE_BASE = 'https://docs.cortex.foundation/problems' as const; TS - cat > "$dest/crates/cortex-api/src/router.rs" <<'RS' - .route("/healthz", get(health::liveness)) - .route("/conversations", get(list)) - .route("/conversations/{id}", get(get).patch(patch)) -RS + cat > "$dest/server/src/api/index.ts" <<'TS' + publicRouter.get("/healthz", liveness).get("/readyz", readiness).get("/startupz", startup); +TS + cat > "$dest/server/src/api/conversations.ts" <<'TS' +export function routes(r: Router): void { + r.get("/conversations", list); + r.get("/conversations/{id}", get).patch("/conversations/{id}", patch); +} +TS + cat > "$dest/server/src/api/admin-insights.ts" <<'TS' +export function adminRoutes(r: Router): void { + r.get("/v1/admin/insights/overview", overview); +} +TS printf '' > "$dest/site/favicon.svg" printf '' > "$dest/site/logo/light.svg" printf '' > "$dest/site/logo/dark.svg" @@ -124,14 +129,29 @@ out="$(cd "$tmp" && CORTEX_CHECK_ROOT="$happy/site" node "$script" 2>&1)" || if CORTEX_CHECK_ROOT="$happy/site" node "$script" "$tmp/absent-backend" >/dev/null 2>&1; then fail "an explicitly requested missing backend must fail" fi +if CORTEX_CHECK_ROOT="$tmp/absent-docs" node "$script" "$happy" >/dev/null 2>&1; then + fail "a missing docs checkout must fail" +fi -# Wrong domain on the Rust constant. +# Wrong domain on the server constant. wrong="$tmp/wrong-base" seed "$wrong" sed -i 's|https://docs.cortex.foundation/problems|https://docs.cortex.sh/problems|' \ - "$wrong/crates/cortex-core/src/error.rs" + "$wrong/server/src/core/error.ts" must_fail "$wrong" "docs.cortex.foundation" +# A documented code the server no longer serves. +dropped="$tmp/dropped-code" +seed "$dropped" +sed -i '/^ internal:/d' "$dropped/server/src/core/error.ts" +must_fail "$dropped" "problems/internal.mdx documents a code that is not in ERROR_CODES" + +# Admin-listener routes are not public API. +adminpath="$tmp/admin-path" +seed "$adminpath" +printf '\n`GET` `/v1/admin/insights/overview`\n' >> "$adminpath/site/problems/not_found.mdx" +must_fail "$adminpath" "admin/insights/overview" + # Missing problem page. missing="$tmp/missing-page" seed "$missing"