Spaces:
Runtime error
Runtime error
| // scripts/check/check-docs-symbols.mjs | |
| // Gate anti-alucinação (docs → código): toda referência a uma rota `/api/...` dentro de | |
| // docs/**/*.md deve resolver para um `route.ts` real em src/app/api/. Pega endpoint | |
| // INVENTADO/obsoleto que a IA escreve em docs/PRs descrevendo uma rota que não existe — | |
| // o padrão recorrente das PRs de docs (ex.: oyi77) que fabricam endpoints/APIs. | |
| // | |
| // Complementa os outros gates anti-alucinação: | |
| // - check-fetch-targets.mjs : fetch("/api/...") na UI → route.ts (código → código) | |
| // - check-openapi-routes.mjs : path da openapi.yaml → route.ts (spec → código) | |
| // - este gate : /api/... na prosa/markdown → route.ts (docs → código) | |
| // | |
| // LOW-NOISE por design: escopo APENAS a paths de rota `/api/...` (sinal mais alto). | |
| // Tudo que é ruído conhecido (superfície proxy OpenAI-compat, refs a arquivos-fonte, | |
| // APIs upstream de terceiros, placeholders) vai para IGNORE com justificativa, NÃO para | |
| // a allowlist. A allowlist congela só drift REAL pré-existente de docs. | |
| // Stale-enforcement (6A.3): entrada em KNOWN_STALE_DOC_REFS que não suprime nenhum miss | |
| // real → gate falha com instrução de remoção (evita furo de regressão silencioso). | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { pathToFileURL } from "node:url"; | |
| import { assertNoStale } from "./lib/allowlist.mjs"; | |
| const ROOT = process.cwd(); | |
| const DOCS = path.join(ROOT, "docs"); | |
| const API = path.join(ROOT, "src/app/api"); | |
| // Padrões que NÃO são rotas internas do OmniRoute (ruído estrutural, não drift). | |
| // Adicione aqui (com justificativa) em vez da allowlist quando uma categoria gera | |
| // falsos positivos — a allowlist é só para endpoints stale REAIS. | |
| const IGNORE = [ | |
| /^\/api\/v1\//, // superfície OpenAI-compat (proxy), não rota interna | |
| /^\/api\/v1beta\//, // superfície Gemini-compat (proxy) | |
| /^\/api\/v0\//, // APIs upstream de terceiros citadas em docs de pesquisa | |
| /^\/api\/v2\//, // idem (deployments etc.) | |
| /^\/api\/(organizations|map-image|graphql|gql)\b/, // APIs de provedores externos documentadas | |
| /your-/i, // placeholder de exemplo | |
| /example/i, // placeholder de exemplo | |
| /\.{3}/, // placeholder "..." | |
| /\{\}/, // placeholder de param vazio | |
| /_(POST|GET|PUT|DELETE|PATCH)$/, // refs estilo trace de rede (gql_POST) | |
| ]; | |
| // Refs a ARQUIVOS-FONTE, não a URLs (ex.: src/app/api/.../route.ts citado em prosa). | |
| // O gate só valida URLs de rota, não caminhos de arquivo. | |
| function isFileRef(p) { | |
| return /\.(ts|tsx|js|mjs|jsx)$/.test(p) || /\/route$/.test(p); | |
| } | |
| // Refs a `/api/...` que NÃO resolvem para rota real, congeladas para triagem | |
| // (catraca: bloqueia QUALQUER nova ref inventada em docs). Estas são achados REAIS de | |
| // drift/alucinação em docs pré-existentes — cada uma precisa de: criar a rota, corrigir | |
| // o path na doc, ou remover a menção. NÃO adicione novas aqui sem justificativa — esse | |
| // é o ponto do gate. Issues de tracking devem ser abertas para cada cluster. | |
| export const KNOWN_STALE_DOC_REFS = new Set([ | |
| // docs/reference/API_REFERENCE.md — guardrails/shadow doc-fiction RESOLVED in #3496: | |
| // GET /api/guardrails + POST /api/guardrails/test are now REAL routes (wrapping the | |
| // existing guardrailRegistry); the fictional enable/disable/logs rows and the entire | |
| // shadow table were removed from the doc (shadow A-B comparison is combo-config + | |
| // /api/combos/metrics). No allowlist entries needed for these anymore. | |
| // docs/research/DISCOVERY_TOOL_DESIGN.md — design doc de feature NÃO implementada | |
| // (Phase 2). Refs INTENCIONAIS: o doc agora traz um banner "⚠️ Not yet implemented | |
| // — Phase 2" acima da tabela de endpoints. Mantidos aqui até a feature existir. — #3498 | |
| "/api/discovery/results", | |
| "/api/discovery/results/:id", | |
| "/api/discovery/scan", | |
| "/api/discovery/verify/:id", | |
| // docs/reference/ENVIRONMENT.md — endpoint UPSTREAM do provedor Blackbox Web, | |
| // citado na descrição de env var (não é rota do OmniRoute): | |
| "/api/chat", | |
| // docs/ops/TUNNELS_GUIDE.md — a doc afirma EXPLICITAMENTE que este endpoint NÃO | |
| // existe ("There is no central /api/settings/tunnels endpoint"); menção pedagógica: | |
| "/api/settings/tunnels", | |
| ]); | |
| function walk(dir, filter, acc = []) { | |
| if (!fs.existsSync(dir)) return acc; | |
| for (const e of fs.readdirSync(dir, { withFileTypes: true })) { | |
| const p = path.join(dir, e.name); | |
| if (e.isDirectory()) walk(p, filter, acc); | |
| else if (filter(e.name)) acc.push(p); | |
| } | |
| return acc; | |
| } | |
| export function collectRouteFiles() { | |
| return new Set( | |
| walk(API, (n) => /^route\.tsx?$/.test(n)).map((p) => | |
| path.relative(ROOT, p).replace(/\\/g, "/") | |
| ) | |
| ); | |
| } | |
| /** Normaliza um segmento dinâmico ({param} / [param] / [...param] / :param) para wildcard. */ | |
| function normSeg(seg) { | |
| if (/^\[\[?\.{3}.+\]\]?$/.test(seg)) return ""; // catch-all [...x] / [[...x]] | |
| if (/^\{[^}]+\}$/.test(seg) || /^\[[^\]]+\]$/.test(seg) || /^:[^/]+$/.test(seg)) return " "; | |
| return seg; | |
| } | |
| // /api/providers/{id}/models → src/app/api/providers/[id]/models/route.ts | |
| // Casa por contagem de segmentos OU por prefixo (uma doc pode citar só o prefixo de | |
| // uma rota mais profunda, ex.: /api/auth descrevendo a família /api/auth/login). Qualquer | |
| // segmento dinâmico ([..]/{..}/:..) casa com um segmento dinâmico real. | |
| export function resolveApiDocPathToRoute(apiPath, routeFiles) { | |
| const segs = apiPath | |
| .replace(/^\//, "") | |
| .replace(/[?#].*$/, "") | |
| .split("/") | |
| .map(normSeg); | |
| for (const rf of routeFiles) { | |
| const rsegs = rf | |
| .replace(/^src\/app\//, "") | |
| .replace(/\/route\.tsx?$/, "") | |
| .split("/"); | |
| const rnorm = rsegs.map((rs) => { | |
| if (/^\[\[?\.{3}.+\]\]?$/.test(rs)) return ""; // catch-all | |
| if (/^\[[^\]]+\]$/.test(rs)) return " "; // [param] | |
| return rs; | |
| }); | |
| const catchAll = rnorm.includes(""); | |
| const effLen = catchAll ? rnorm.indexOf("") : rnorm.length; | |
| if (!catchAll && segs.length > rnorm.length) continue; // doc mais profunda que a rota | |
| if (catchAll && segs.length < effLen) continue; | |
| const cmpLen = Math.min(segs.length, effLen || rnorm.length); | |
| let match = true; | |
| for (let i = 0; i < cmpLen; i++) { | |
| const rs = rnorm[i]; | |
| if (rs === "") break; // catch-all absorve o resto | |
| if (!(rs === segs[i] || rs === " " || segs[i] === " ")) { | |
| match = false; | |
| break; | |
| } | |
| } | |
| if (match) return true; | |
| } | |
| return false; | |
| } | |
| /** Limpa o path capturado: remove pontuação/ênfase de prosa, fecha brackets pendentes. */ | |
| function cleanCapturedPath(raw) { | |
| let p = raw.replace(/[.,:;_)>]+$/, ""); | |
| const ob = (p.match(/\[/g) || []).length; | |
| const cb = (p.match(/\]/g) || []).length; | |
| const oc = (p.match(/\{/g) || []).length; | |
| const cc = (p.match(/\}/g) || []).length; | |
| if (ob !== cb || oc !== cc) { | |
| // segmento final truncado pelo regex (bracket aberto sem fechar na prosa) → descarta | |
| p = p.replace(/\/[^/]*[[{][^/]*$/, ""); | |
| } | |
| return p.replace(/\/$/, ""); // remove barra final (forma de prefixo) | |
| } | |
| // /api/... só conta como URL quando NÃO é a cauda de um caminho de arquivo-fonte | |
| // (src/lib/api/, @/app/api/, app/api/). O grupo 2 é o path. | |
| const API_PATH_RE = /(^|[^A-Za-z0-9_/])(\/api\/[A-Za-z0-9_\-/{}\[\].:]+)/g; | |
| /** Extrai os paths /api/... distintos de um arquivo markdown (forma URL, não arquivo). */ | |
| export function extractDocApiPaths(src) { | |
| const out = new Set(); | |
| let m; | |
| API_PATH_RE.lastIndex = 0; | |
| while ((m = API_PATH_RE.exec(src))) { | |
| const p = cleanCapturedPath(m[2]); | |
| if (p && p !== "/api") out.add(p); | |
| } | |
| return [...out]; | |
| } | |
| /** | |
| * Núcleo puro/testável. | |
| * @param {{file: string, paths: string[]}[]} docPathsByFile | |
| * @param {Set<string>} routeFiles conjunto de "src/app/api/.../route.ts" | |
| * @param {Set<string>} allowlist paths stale congelados | |
| * @returns {string[]} misses no formato "file → /api/path" | |
| */ | |
| export function findStaleDocApiRefs(docPathsByFile, routeFiles, allowlist) { | |
| const misses = []; | |
| for (const { file, paths } of docPathsByFile) { | |
| for (const p of paths) { | |
| if (IGNORE.some((rx) => rx.test(p))) continue; | |
| if (isFileRef(p)) continue; | |
| if (allowlist.has(p)) continue; | |
| if (!resolveApiDocPathToRoute(p, routeFiles)) { | |
| misses.push(`${file} → ${p}`); | |
| } | |
| } | |
| } | |
| return misses; | |
| } | |
| function main() { | |
| const routeFiles = collectRouteFiles(); | |
| // docs/i18n/** são espelhos auto-gerados das docs canônicas — validar só o canônico | |
| // evita 40× de ruído duplicado (e os mirrors herdam qualquer fix do canônico). | |
| // docs/superpowers/** são planos internos de implementação (snapshots históricos | |
| // de intenção — podem citar rotas planejadas/abandonadas), não claims sobre o | |
| // código atual; fora do escopo do gate (drift surgiu no ciclo v3.8.18). | |
| const docFiles = walk(DOCS, (n) => /\.md$/.test(n)).filter((f) => { | |
| const rel = path.relative(ROOT, f).replace(/\\/g, "/"); | |
| return !rel.startsWith("docs/i18n/") && !rel.startsWith("docs/superpowers/"); | |
| }); | |
| const docPathsByFile = docFiles.map((f) => ({ | |
| file: path.relative(ROOT, f).replace(/\\/g, "/"), | |
| paths: extractDocApiPaths(fs.readFileSync(f, "utf8")), | |
| })); | |
| // Live misses BEFORE allowlist filtering — used for stale-enforcement. | |
| // The paths (not "file → path" strings) are the unit that the allowlist keys on. | |
| const allMisses = findStaleDocApiRefs(docPathsByFile, routeFiles, new Set()); | |
| const liveMissPaths = allMisses.map((m) => m.split(" → ")[1]); | |
| assertNoStale(KNOWN_STALE_DOC_REFS, liveMissPaths, "check-docs-symbols"); | |
| const misses = findStaleDocApiRefs(docPathsByFile, routeFiles, KNOWN_STALE_DOC_REFS); | |
| if (misses.length) { | |
| console.error( | |
| `[check-docs-symbols] ${misses.length} ref(s) /api em docs sem rota real:\n` + | |
| misses.map((m) => " ✗ " + m).join("\n") + | |
| `\n → crie o route.ts, corrija o path na doc, ou (se for upstream/placeholder)` + | |
| ` adicione um padrão a IGNORE com justificativa. NÃO adicione à allowlist sem` + | |
| ` confirmar que é drift pré-existente real.` | |
| ); | |
| process.exitCode = 1; | |
| } | |
| if (!process.exitCode) { | |
| console.log( | |
| `[check-docs-symbols] OK — ${docFiles.length} docs canônicas, ` + | |
| `${routeFiles.size} rotas conhecidas, ${KNOWN_STALE_DOC_REFS.size} stale congeladas` | |
| ); | |
| } | |
| } | |
| if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main(); | |