mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Collapse duplicate CI spend while keeping each gate's existence reason: - quality.yml: TIA __RUN_ALL__ defers full unit to fast-unit 4-shard (#6781); path filters via classify-pr-changes; docs-gates split; draft skip - ci.yml: wire docs/i18n/code path filters; ESLint JSON artifact for quality-gate; drop advisory typecheck:noimplicit; float actions/cache@v6 - TIA parity: memory/usage/combo/serial; **/*.test.mjs any depth; electron/bin no longer force unit __RUN_ALL__ - check:complexity-ratchets: one ESLint walk, ruleId-isolated baselines + cache - check:api-docs-refs + lib/apiRoutes: shared API route inventory - husky pre-push: intentionally light (gates live in pre-commit); CLAUDE.md + QUALITY_GATES.md docs synced - collect-metrics / lint:json: path.resolve cache path; Windows-safe eslint bin - env-doc allowlist for ESLINT_RESULTS_JSON / COMPLEXITY_ESLINT_REPORT - release-green --full-ci expects check:api-docs-refs (not docs-symbols alone) Tests: select-impacted, classify-pr-changes, api-routes lib, complexity-rule-count, validate-release-green. Reconciled after #6781 (fast-unit 2→4 shards) per maintainer request on #6716. Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
101 lines
4.0 KiB
JavaScript
101 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
// scripts/check/check-openapi-routes.mjs
|
|
// Gate anti-alucinação (docs): toda `path` documentada em docs/openapi.yaml
|
|
// deve resolver para um route.ts real em src/app/api/. Pega endpoint INVENTADO/obsoleto
|
|
// na spec (a IA escreve docs descrevendo rota que não existe). Complementa
|
|
// check-openapi-coverage.mjs (que mede a direção inversa: % de rotas documentadas).
|
|
// Stale-enforcement (6A.3): entrada em KNOWN_STALE_SPEC que não suprime nenhum path
|
|
// órfão 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 * as yaml from "js-yaml";
|
|
import { reportStaleEntries } from "./lib/allowlist.mjs";
|
|
import { apiRoot, collectApiRouteUrlPaths } from "./lib/apiRoutes.mjs";
|
|
|
|
const ROOT = process.cwd();
|
|
const OPENAPI_PATH = path.join(ROOT, "docs", "openapi.yaml");
|
|
|
|
// Entradas da spec sem rota real, congeladas para triagem (catraca: bloqueia NOVAS).
|
|
export const KNOWN_STALE_SPEC = new Set([
|
|
// openapi.yaml documenta um state por-agente, mas a rota real é o state GLOBAL
|
|
// (/api/tools/agent-bridge/state); por-agente só há /{id}, /{id}/detect, /mappings, /dns.
|
|
]);
|
|
|
|
/** Normaliza qualquer {param} para {} para casar independente do nome do parâmetro. */
|
|
export function normalizeParams(p) {
|
|
return p.replace(/\{[^}]+\}/g, "{}");
|
|
}
|
|
|
|
/** Paths da spec que não casam com nenhuma rota implementada (param-insensitive). */
|
|
export function findSpecPathsWithoutRoute(specPaths, implPaths) {
|
|
const impl = new Set(implPaths.map(normalizeParams));
|
|
return specPaths.filter((p) => !impl.has(normalizeParams(p)));
|
|
}
|
|
|
|
/**
|
|
* @param {{ root?: string, openapiPath?: string, implPaths?: string[] }} [opts]
|
|
* @returns {{ ok: boolean, exitCode: number, message: string }}
|
|
*/
|
|
export function runOpenapiRoutesCheck(opts = {}) {
|
|
const root = opts.root || ROOT;
|
|
const openapiPath = opts.openapiPath || path.join(root, "docs", "openapi.yaml");
|
|
if (!fs.existsSync(openapiPath)) {
|
|
return {
|
|
ok: false,
|
|
exitCode: 1,
|
|
message: `[openapi-routes] FAIL — openapi.yaml não encontrado: ${openapiPath}`,
|
|
};
|
|
}
|
|
if (!fs.existsSync(apiRoot(root))) {
|
|
return {
|
|
ok: false,
|
|
exitCode: 1,
|
|
message: `[openapi-routes] FAIL — API root not found: ${apiRoot(root)}`,
|
|
};
|
|
}
|
|
|
|
const raw = yaml.load(fs.readFileSync(openapiPath, "utf-8"));
|
|
const specPaths = Object.keys(raw.paths || {}).filter((p) => p.startsWith("/api"));
|
|
const implPaths = opts.implPaths || collectApiRouteUrlPaths(root);
|
|
|
|
const liveOrphans = findSpecPathsWithoutRoute(specPaths, implPaths);
|
|
const stale = reportStaleEntries(KNOWN_STALE_SPEC, liveOrphans, "openapi-routes");
|
|
const orphans = liveOrphans.filter((p) => !KNOWN_STALE_SPEC.has(p));
|
|
|
|
const parts = [];
|
|
if (stale.length) {
|
|
parts.push(
|
|
`[openapi-routes] ${stale.length} entrada(s) obsoleta(s) na allowlist ` +
|
|
`— a violação foi corrigida; REMOVA a entrada para travar a correção:\n` +
|
|
stale.map((e) => ` ✗ ${e}`).join("\n")
|
|
);
|
|
}
|
|
if (orphans.length) {
|
|
parts.push(
|
|
`[openapi-routes] ${orphans.length} path(s) documentado(s) sem rota real:\n` +
|
|
orphans.map((p) => " ✗ " + p).join("\n") +
|
|
`\n → crie a rota, corrija/remova a entrada na spec, ou adicione a KNOWN_STALE_SPEC com justificativa.`
|
|
);
|
|
}
|
|
if (parts.length) {
|
|
return { ok: false, exitCode: 1, message: parts.join("\n") };
|
|
}
|
|
return {
|
|
ok: true,
|
|
exitCode: 0,
|
|
message: `[openapi-routes] OK — ${specPaths.length} paths na spec, todos com rota real (${implPaths.length} rotas)`,
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
// Keep assertNoStale side-effect path for CLI parity with other gates when
|
|
// runOpenapiRoutesCheck is not used alone — here we print structured result.
|
|
const result = runOpenapiRoutesCheck();
|
|
if (result.ok) console.log(result.message);
|
|
else console.error(result.message);
|
|
process.exit(result.exitCode);
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();
|