Files
OmniRoute/src/lib/agentSkills/openapiParser.ts
Diego Rodrigues de Sa e Souza 059dbe9f13 feat(quality): no-new-warnings por PR — ESLint bulk suppressions + lint-guard fork-condicional (Pacote 4) (#6218)
* 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.
2026-07-04 21:25:17 -03:00

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}`);
}