mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
* feat(quality): no-new-warnings per PR via native ESLint bulk suppressions Pacote 4 do plano mestre testes+CI (aprovado 2026-07-04). O ratchet de eslintWarnings so rodava no CI pesado (release-PR) -> o drift acumulava invisivel e explodia na release (+41/+37/+88 por ciclo, rebaselinado as cegas — historico no proprio quality-baseline.json). Modelo novo (SonarSource Clean-as-You-Code + ESLint bulk suppressions nativo >=9.24): - config/quality/eslint-suppressions.json congela a divida existente por arquivo+regra: 476 arquivos / 4.273 violacoes. - npm run lint + lint-staged (pre-commit) + novo job lint-guard no quality.yml rodam suppressions-aware: violacao NOVA fica vermelha NO PR que a introduz (bulk suppressions ainda eleva estouros de baseline por arquivo a error). - 3 regras warn promovidas a error em src/** (react-hooks/exhaustive-deps, @next/next/no-img-element, import/no-anonymous-default-export) — divida existente congelada, ocorrencia nova = erro imediato. - collect-metrics mede sob o baseline congelado -> a metrica eslintWarnings vira 'divida liquida nova' (~0 em regime); baseline apertado 4279->0 no mesmo PR (exigencia do require-tighten). Aperto do ESTOQUE congelado: npx eslint . --prune-suppressions na reconciliacao da release. - Principio Zero: lint-guard usa continue-on-error para PR de FORK (report-only; a campanha /green-prs aplica o fix via co-autoria) — bloqueante so para branches internas, a origem real do drift. Validacao: negativo (any novo em tests/) exit 1; negativo (img em src/, regra promovida) exit 1; positivo escopado exit 0; baseline gerado por --suppress-all no tip (tree inteiro passa por construcao); YAML js-yaml ok. * fix(quality): clear the 6 residual warnings so lint-guard runs clean at --max-warnings 0 The committed baseline still let 6 warnings through the lint-guard gate: 5 now-unused inline eslint-disable directives (the file-level suppressions made them redundant — removed via eslint --fix, suppressions regenerated to absorb the re-exposed occurrences) and 1 anonymous default export in tests/load/k6-soak.js (outside the src/** severity-override scope — named the k6 scenario function instead). Verified on the clean tree: lint-guard exit=0; any-canary (new 'const x: any' in open-sse) exit=1 — the gate bites on NEW violations while the 4,273 frozen ones stay suppressed (476 files). * fix(ci): lint-guard continue-on-error must be boolean on non-PR events github.event.pull_request is undefined on workflow_dispatch — the bare property expression made the job fail at plan time (run 28722888456: 4 jobs green, run red, lint-guard never materialized). Guard with event_name check so the expression is always boolean: PR de fork = report-only (Principio Zero), resto = blocking.
202 lines
6.2 KiB
TypeScript
202 lines
6.2 KiB
TypeScript
/**
|
|
* openapiParser.ts — parses docs/openapi.yaml to extract endpoint info
|
|
* grouped by SkillArea. Used by the catalog and the generator.
|
|
*
|
|
* Reads the OpenAPI YAML synchronously at runtime (same pattern as
|
|
* src/app/api/openapi/spec/route.ts). Does NOT fetch via HTTP to remain
|
|
* usable as a standalone script/CI tool (D15).
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import * as yaml from "js-yaml";
|
|
import type { SkillArea } from "./types";
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export interface OpenapiPath {
|
|
/** HTTP method (uppercase): "GET", "POST", etc. */
|
|
method: string;
|
|
/** OpenAPI path template, e.g. "/api/providers/{id}" */
|
|
path: string;
|
|
/** Summary from the operation object */
|
|
summary: string;
|
|
/** Description from the operation object (may be absent) */
|
|
description?: string;
|
|
/** OpenAPI tags */
|
|
tags: string[];
|
|
}
|
|
|
|
export interface ParsedOpenapi {
|
|
/** All endpoints keyed by "<METHOD> <path>" */
|
|
paths: Map<string, OpenapiPath>;
|
|
/** Endpoints grouped by SkillArea (only API-mapped areas) */
|
|
areas: Map<SkillArea, OpenapiPath[]>;
|
|
}
|
|
|
|
// ── Mapping: path prefix → SkillArea ────────────────────────────────────────
|
|
|
|
/**
|
|
* Maps an API path prefix to the corresponding SkillArea.
|
|
* Order matters: more specific prefixes must come before generic ones.
|
|
*/
|
|
const PATH_AREA_MAP: Array<[string, SkillArea]> = [
|
|
// Auth
|
|
["/api/auth", "auth"],
|
|
["/api/session", "auth"],
|
|
// Providers
|
|
["/api/providers", "providers"],
|
|
["/api/provider-nodes", "providers"],
|
|
["/api/provider-models", "providers"],
|
|
// Models
|
|
["/api/v1/models", "models"],
|
|
["/api/models", "models"],
|
|
// Combos / routing
|
|
["/api/combos", "combos-routing"],
|
|
["/api/fallback", "combos-routing"],
|
|
// API Keys
|
|
["/api/keys", "api-keys"],
|
|
// Usage logs
|
|
["/api/usage", "usage-logs"],
|
|
// Budget / rate limit
|
|
["/api/rate-limit", "budget"],
|
|
["/api/budget", "budget"],
|
|
// Settings
|
|
["/api/settings", "settings"],
|
|
["/api/tags", "settings"],
|
|
// Proxies
|
|
["/api/settings/proxy", "proxies"],
|
|
// Cache
|
|
["/api/cache", "cache"],
|
|
// Compression / RTK
|
|
["/api/settings/compression", "compression"],
|
|
["/api/compression", "compression"],
|
|
["/api/context/rtk", "context-rtk"],
|
|
// Resilience
|
|
["/api/monitoring", "resilience"],
|
|
["/api/provider-metrics", "resilience"],
|
|
["/api/circuit-breakers", "resilience"],
|
|
// CLI tools
|
|
["/api/cli-tools", "cli-tools"],
|
|
// Tunnels
|
|
["/api/tunnel", "tunnels"],
|
|
// Sync / cloud
|
|
["/api/cloud", "sync-cloud"],
|
|
["/api/sync", "sync-cloud"],
|
|
// DB backups
|
|
["/api/system", "db-backups"],
|
|
["/api/backup", "db-backups"],
|
|
// Webhooks
|
|
["/api/webhooks", "webhooks"],
|
|
// MCP
|
|
["/api/mcp", "mcp"],
|
|
// A2A
|
|
["/a2a", "agents-a2a"],
|
|
// Version manager
|
|
["/api/services", "version-manager"],
|
|
["/api/version", "version-manager"],
|
|
// Inference (catch-all for /api/v1/* proxy endpoints)
|
|
["/api/v1", "inference"],
|
|
];
|
|
|
|
// ── HTTP methods recognised as operations ────────────────────────────────────
|
|
|
|
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const;
|
|
|
|
// ── Parser ───────────────────────────────────────────────────────────────────
|
|
|
|
function resolveArea(urlPath: string): SkillArea | null {
|
|
for (const [prefix, area] of PATH_AREA_MAP) {
|
|
if (
|
|
urlPath === prefix ||
|
|
urlPath.startsWith(prefix + "/") ||
|
|
urlPath.startsWith(prefix + "{")
|
|
) {
|
|
return area;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function extractOperations(pathsObj: Record<string, any>): OpenapiPath[] {
|
|
const ops: OpenapiPath[] = [];
|
|
|
|
for (const [urlPath, pathItem] of Object.entries(pathsObj)) {
|
|
if (!pathItem || typeof pathItem !== "object") continue;
|
|
|
|
for (const method of HTTP_METHODS) {
|
|
const operation = pathItem[method];
|
|
if (!operation || typeof operation !== "object") continue;
|
|
|
|
ops.push({
|
|
method: method.toUpperCase(),
|
|
path: urlPath,
|
|
summary: String(operation.summary ?? ""),
|
|
description: operation.description ? String(operation.description) : undefined,
|
|
tags: Array.isArray(operation.tags) ? operation.tags.map(String) : [],
|
|
});
|
|
}
|
|
}
|
|
|
|
return ops;
|
|
}
|
|
|
|
/**
|
|
* Parses `docs/openapi.yaml` and returns:
|
|
* - `paths`: all operations keyed by `"METHOD /path"`
|
|
* - `areas`: operations grouped by SkillArea (api skills only)
|
|
*
|
|
* Reads the file synchronously so it can be called from both server context
|
|
* and standalone scripts without async machinery.
|
|
*/
|
|
export function parseOpenapi(): ParsedOpenapi {
|
|
const yamlPath = path.resolve(process.cwd(), "docs", "openapi.yaml");
|
|
let rawContent: string;
|
|
|
|
try {
|
|
rawContent = fs.readFileSync(yamlPath, "utf-8");
|
|
} catch (err) {
|
|
throw new Error(
|
|
`openapiParser: could not read ${yamlPath}. ` +
|
|
`Run from project root. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
);
|
|
}
|
|
|
|
const doc = yaml.load(rawContent) as Record<string, any>;
|
|
|
|
if (!doc || typeof doc !== "object") {
|
|
throw new Error("openapiParser: parsed YAML is not an object");
|
|
}
|
|
|
|
const pathsObj = doc.paths ?? {};
|
|
const operations = extractOperations(pathsObj);
|
|
|
|
const paths = new Map<string, OpenapiPath>();
|
|
const areas = new Map<SkillArea, OpenapiPath[]>();
|
|
|
|
for (const op of operations) {
|
|
const key = `${op.method} ${op.path}`;
|
|
paths.set(key, op);
|
|
|
|
const area = resolveArea(op.path);
|
|
if (area) {
|
|
if (!areas.has(area)) {
|
|
areas.set(area, []);
|
|
}
|
|
areas.get(area)!.push(op);
|
|
}
|
|
}
|
|
|
|
return { paths, areas };
|
|
}
|
|
|
|
/**
|
|
* Returns endpoint strings for a given SkillArea, suitable for `AgentSkill.endpoints`.
|
|
* Format: `"GET /api/providers/{id}"`.
|
|
*/
|
|
export function getEndpointsForArea(area: SkillArea): string[] {
|
|
const { areas } = parseOpenapi();
|
|
const ops = areas.get(area) ?? [];
|
|
return ops.map((op) => `${op.method} ${op.path}`);
|
|
}
|