Files
OmniRoute/scripts/check/check-docs-symbols.mjs
Diego Rodrigues de Sa e Souza bf8b56b29f Release v3.8.20 (#3547)
* chore(release): open v3.8.20 development cycle

* fix(images): prefer bare combos over image aliases (#3527)

Integrated into release/v3.8.20

* fix(translator): map Codex local_shell tool (#3534)

Integrated into release/v3.8.20

* fix(usage): make opencode-go quota fetcher fail-open instead of throwing 500 (#3522)

Integrated into release/v3.8.20

* Fix Runtime page breaker state rendering (#3533)

Integrated into release/v3.8.20

* Expose provider breaker degradation threshold setting (#3535)

Integrated into release/v3.8.20

* fix(executor): strip provider prefix from versioned built-in tool model field (#3532)

Integrated into release/v3.8.20

* feat(providers): add Claude Fable 5 support (#3524)

Integrated into release/v3.8.20

* feat(resilience): add global provider cooldown tracking to prevent combo re-walking (#3556)

Integrated into release/v3.8.20 (default OFF, opt-in)

* fix(translator): scope thoughtSignature bypass to Antigravity/CLI only (#3560)

Integrated into release/v3.8.20. Co-authored-by: Six7Day <six7day@gmail.com>

* fix(routing): normalize thinking:disabled for combo-substituted models that reject it (#3554) (#3563)

Integrated into release/v3.8.20

* fix(usage): accept 0/empty budget limits so the dashboard can save and clear (#3537) (#3564)

Integrated into release/v3.8.20

* docs(changelog): credit @Six7Day for #3560 thoughtSignature fix (#3414)

The #3560 squash co-author trailer landed inline (unparsed by GitHub), so add
an explicit CHANGELOG credit ensuring @Six7Day (original #3414) and @oyi77 are
on the public record for the Gemini thoughtSignature fix.

* fix(gamification): dedup badge unlock via user_badges so events don't re-fire every request (#3472) (#3565)

Integrated into release/v3.8.20

* fix(routing): pass through 'auto' keyword on codex /v1/responses instead of rewriting to codex/auto (#3509) (#3566)

Integrated into release/v3.8.20

* fix(cli-tools): normalize apiKey null in guide-settings schema so cloud-mode config saves (#3552) (#3567)

Integrated into release/v3.8.20

* fix(catalog): reclassify PublicAI from keyless to one-time-initial (requires API key) (#3558) (#3568)

Integrated into release/v3.8.20

* fix(gemini-web): surface missing Playwright browser as actionable 503 + cooldown hint, not a retryable 500 loop (#3516) (#3570)

Integrated into release/v3.8.20

* fix(security): sanitize raw err.message in web executors + embeddings/search response bodies (Rule #12) (#3494, #3495) (#3573)

Integrated into release/v3.8.20

* fix(dashboard): point CustomHostsManager + FeatureFlagsGrid at real routes (#3486, #3487) (#3574)

Integrated into release/v3.8.20

* chore(providers): remove dead krutrim entry (#3483) + docs(api): fix agent-bridge per-agent state route (#3489) (#3575)

Integrated into release/v3.8.20

* docs(api): correct API_REFERENCE.md paths for skills/plugins/admin/cache/acp/system-info (#3497) (#3577)

Integrated into release/v3.8.20

* fix(proxy): drive SOCKS5 UI option from runtime ENABLE_SOCKS5_PROXY, not build-time NEXT_PUBLIC (#3508) (#3579)

Integrated into release/v3.8.20

* fix(playground): filter playground models by node prefix so custom-endpoint models appear (#3505) (#3581)

Integrated into release/v3.8.20

* fix(usage): show an informative message instead of a blank Kiro quota card when no usage breakdown (#3506) (#3582)

Integrated into release/v3.8.20

* docs(changelog): add the #3506 Kiro quota entry (missed in #3582 due to a stale-base CHANGELOG anchor) (#3583)

Integrated into release/v3.8.20

* fix(auto-update): use stable PROJECT_ROOT walker, not frozen process.cwd() (#3561)

Integrated into release/v3.8.20. Auto-update PROJECT_ROOT now uses a stable __dirname-anchored upward walker instead of the no-op process.cwd() resolver.

* fix: address PR #3518 review comments (lifecycle hooks, regex, indentation, route params) (#3562)

Integrated into release/v3.8.20. Addresses #3518 review: regex literals, logs/[id] route params (Next 16), indentation, and wires plugin lifecycle hooks (onInstall/onActivate/onDeactivate/onUninstall) in the loader so manager.ts can register them. Adds Rule #18 regression test.

* docs(changelog): credit @ViFigueiredo (#3423) for PROJECT_ROOT + log #3561/#3562 (v3.8.20)

* fix: openai to gemini incorrectly translates historical tool calls into text (#3569)

Integrated into release/v3.8.20. Standard Gemini direct path now maps historical tool calls to native functionCall/functionResponse parts (signaturelessToolCallMode: native) instead of inert text — validated against the real Gemini API (gemini-2.5-flash returns 200 for signatureless native functionCall, even with tools+thinking; Hard Rule #18). Eliminates the text-serialization leak. Antigravity/CLI sentinel path (#3560) untouched.

* docs(changelog)+test: reconcile standard-Gemini native mode (#3569) — update round-2 rationale comment + log VPS validation

* docs(changelog): reconcile v3.8.20 — add 9 missing bullets + move [Unreleased] to versioned section

* docs(changelog): complete v3.8.20 reconciliation — 27 bullets, 11 contributors

---------

Co-authored-by: Alexander Averyanov <alex@averyan.ru>
Co-authored-by: Hakan Kurşun <bykamaka@gmail.com>
Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Giorgos Giakoumettis <giorgos@yiakoumettis.gr>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
2026-06-10 13:49:08 -03:00

225 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// 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.
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
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 entries fixed in separate issues:
"/api/guardrails", // sem dir de API guardrails (feature server-side, sem rota REST) — #3496
"/api/guardrails/[id]/disable",
"/api/guardrails/[id]/enable",
"/api/guardrails/logs",
"/api/guardrails/test",
"/api/shadow", // sem dir de API shadow (shadow routing não tem rota REST) — #3498
"/api/shadow/[id]",
"/api/shadow/[id]/results",
"/api/shadow/metrics",
// docs/research/DISCOVERY_TOOL_DESIGN.md — design doc de feature NÃO implementada: — #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")),
}));
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.exit(1);
}
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();