AIQUAA QA Core

Núcleo de QA reutilizable para servidores MCP, que proporciona análisis de requisitos, trazabilidad, cobertura, planificación de cambios, parcheo seguro e integraciones con GitHub.

Documentación

@aiquaa/mcp-qa-core

Núcleo reutilizable de arquitectura hexagonal compartido por los servidores MCP de QA de AIQUAA (aiquaa-playwright-mcp-server, aiquaa-api-quality-mcp-server, aiquaa-performance-mcp-server, y futuros MCPs de Hurl/Pact/k6/Selenium/Appium/REST Assured).

Centraliza las partes de un servidor MCP de QA de AIQUAA que no tienen relación con una herramienta de prueba específica: normalización de requisitos, trazabilidad, agregación de cobertura, planificación de cambios, generación de parches, políticas de seguridad y adaptadores para GitHub, AIQUAA, CodeGraph y Engram. Nunca importa Playwright, Postman, Newman, JMeter, Hurl, k6, Selenium o Appium.

Propósito y límites

El núcleo hace: normalizar requisitos y criterios de aceptación; modelar reglas de negocio; construir y consultar un grafo de trazabilidad requisito→evidencia; agregar cobertura desde evaluadores específicos de herramientas; planificar cambios (crear/extender/modificar/mantener/desaprobar/eliminar/bloquear) bajo reglas de seguridad; generar/validar/aplicar diffs unificados; escanear secretos y aplicar políticas de ruta/escritura/ejecución/host; comunicarse con GitHub, AIQUAA, CodeGraph y Engram a través de puertos; y dar forma consistente a las respuestas de las herramientas MCP.

El núcleo no hace: generar especificaciones de Playwright o Gherkin, construir colecciones de Postman, ejecutar Newman o JMeter, abrir un navegador, interpretar localizadores, calcular percentiles JTL o generar XML JMX. Esas tareas permanecen en el MCP consumidor, conectadas a través del contrato QaToolPlugin.

Consulta ARCHITECTURE.md para el desglose de capas y MIGRATION.md para el inventario de lo que se movió fuera de aiquaa-playwright-mcp-server y por qué.

Instalación

npm install @aiquaa/mcp-qa-core zod

Se requiere Node.js 20+. El paquete es solo ESM, se puede reducir con tree-shaking, no tiene efectos secundarios al importarse y envía exportaciones de subruta para que nunca necesites acceder a dist/:

import { createQaCore } from "@aiquaa/mcp-qa-core";
import { CoverageEngine } from "@aiquaa/mcp-qa-core/coverage";
import { TraceabilityEngine } from "@aiquaa/mcp-qa-core/traceability";
import { ChangePlanner } from "@aiquaa/mcp-qa-core/changes";
import { generateUnifiedDiff, PatchValidator } from "@aiquaa/mcp-qa-core/patches";
import { LocalGitRepositoryAdapter } from "@aiquaa/mcp-qa-core/repository";
import { OctokitPullRequestAdapter, GitHubRepositoryAdapter } from "@aiquaa/mcp-qa-core/github";
import { HttpAiquaaAdapter } from "@aiquaa/mcp-qa-core/aiquaa";
import { CodeGraphCliAdapter } from "@aiquaa/mcp-qa-core/codegraph";
import { EngramCliAdapter } from "@aiquaa/mcp-qa-core/memory";
import { SecretScanner, PathPolicy } from "@aiquaa/mcp-qa-core/security";
import { createToolSuccess, toMcpStructuredContent } from "@aiquaa/mcp-qa-core/mcp";
import { createQaCoreTestHarness } from "@aiquaa/mcp-qa-core/testing";

Inicio rápido

import { createQaCore, createDefaultConfig } from "@aiquaa/mcp-qa-core";

const qaCore = createQaCore({ config: createDefaultConfig() });

const [requirement] = qaCore.requirements.normalize([
  { externalId: "REQ-42", title: "Login", description: "User can log in with email and password." },
]);

const context = qaCore.createContext({ operationId: "generate-123", dryRun: true });

Contrato de plugin

Cada MCP consumidor implementa QaToolPlugin<TInput, TAnalysis, TArtifact> — la costura que mantiene la lógica específica de la herramienta fuera del núcleo:

import type { QaToolPlugin, CoverageReport, ChangePlan } from "@aiquaa/mcp-qa-core";

export const examplePlugin: QaToolPlugin<ExampleInput, ExampleAnalysis, ExampleArtifact> = {
  name: "example",
  version: "1.0.0",

  canHandle(input) {
    return input.type === "example";
  },

  async analyze(input, context) {
    return { source: input, findings: [] };
  },

  async evaluateCoverage(analysis, context): Promise<CoverageReport> {
    return { items: [], summary: { total: 0, covered: 0, partiallyCovered: 0, uncovered: 0, blocked: 0, outdated: 0, percentage: 0 } };
  },

  async planChanges(analysis, coverage, context): Promise<ChangePlan> {
    return { strategy: "keep", changes: [], assumptions: [], warnings: [], blockedReasons: [] };
  },

  async generateArtifacts() {
    return [];
  },
};

const result = await qaCore.runPlugin(examplePlugin, input);
// result.analysis, result.coverage, result.changePlan, result.artifacts

Consulta MIGRATION.md para un ejemplo completo con forma de Playwright.

Cobertura

El núcleo nunca decide qué significa "cubierto" para una herramienta específica — agrega los evaluadores que tú proporcionas:

import { CoverageEngine } from "@aiquaa/mcp-qa-core/coverage";

const engine = new CoverageEngine([myToolSpecificEvaluator]);
const report = await engine.evaluate(myContext);
// report.summary.percentage, report.items[]

Trazabilidad

import { TraceabilityEngine } from "@aiquaa/mcp-qa-core/traceability";

const engine = new TraceabilityEngine();
const graph = engine.build({ requirements, scenarios, artifacts, executionResults, evidence });

engine.findUncoveredCriteria(graph, allCriteria);
engine.findOrphanArtifacts(graph, allArtifacts);
engine.findBrokenLinks(graph, { requirement: new Set(requirementIds) });

Planificación de cambios y parches

import { ChangePlanner } from "@aiquaa/mcp-qa-core/changes";
import { PatchGenerator, PatchValidator, PatchApplier } from "@aiquaa/mcp-qa-core/patches";

const plan = new ChangePlanner().plan({
  candidates: [{ targetPath: "tests/login.spec.ts", decision: "create", reason: "covers REQ-42", requirementIds: ["req-42"], businessRuleIds: [], risk: "low" }],
  existingArtifactPaths: [],
  requestedScope: ["tests"],
});
// plan.strategy, plan.changes[].decision ("create" | "block" | …), plan.blockedReasons

ChangePlanner rechaza artefactos duplicados, sobrescrituras ciegas (modify/delete sin hasReadExistingContent: true), rutas fuera de alcance, eliminaciones injustificadas y cambios sin evidencia de requisito/regla de negocio — devuelve "block" en su lugar.

Repositorio y solicitudes de extracción

import { GitHubRepositoryAdapter, OctokitPullRequestAdapter, runPullRequestFlow } from "@aiquaa/mcp-qa-core/github";

const pullRequests = new OctokitPullRequestAdapter({ octokit });
const result = await runPullRequestFlow(pullRequests, {
  repository: { owner: "aiquaa", repo: "demo" },
  baseBranch: "main",
  branchName: "feat/login-coverage",
  commitMessage: "test: add login coverage",
  title: "Add login coverage",
  body: "Generated by the QA MCP.",
  files: changePlanFiles,
  // dryRun and draft both default to true — nothing is written until you opt out.
});

Seguridad

import { SecretScanner, PathPolicy, RepositoryWritePolicy } from "@aiquaa/mcp-qa-core/security";

const scanner = new SecretScanner();
scanner.scanText(fileContent); // → SecretFinding[], never the full secret value

const pathPolicy = new PathPolicy({ allowedRoots: [projectRoot] });
pathPolicy.assertSafe(candidatePath); // throws UnsafePathError on traversal, .git, node_modules, or escape

RepositoryWritePolicy, ExecutionPolicy y HostPolicy aplican el mismo valor predeterminado "explícito y acotado" a escrituras en el repositorio, ejecución de procesos externos y llamadas HTTP salientes, respectivamente.

Utilidades de prueba

import { createQaCoreTestHarness } from "@aiquaa/mcp-qa-core/testing";

const harness = createQaCoreTestHarness({ files: { "src/controller.ts": "..." } });
const result = await harness.runPlugin(myPlugin, input);

expect(result.changePlan.strategy).toBe("extend");
expect(harness.pullRequests.commits).toHaveLength(0); // dryRun by default

El arnés conecta InMemoryFileSystemAdapter, InMemoryRepositoryAdapter, InMemoryPullRequestAdapter, InMemoryAiquaaAdapter, InMemoryProjectMemoryAdapter y TestLoggerAdapter a un QaCore real — sin red, sin disco, sin proceso externo.

Respuestas MCP

import { createToolSuccess, toMcpStructuredContent } from "@aiquaa/mcp-qa-core/mcp";

const response = createToolSuccess({ operationId: context.operationId, summary: "Generated 3 artifacts", data: artifacts });
return toMcpStructuredContent(response, "files"); // { content: [...], isError: false, structuredContent: artifacts }

Configuración

import { loadConfigFromEnvironment, validateConfig } from "@aiquaa/mcp-qa-core";

const config = loadConfigFromEnvironment(process.env);
const validated = validateConfig(config); // Result<QaCoreConfig, ConfigurationError>

Variables de entorno reconocidas: GITHUB_TOKEN, GITHUB_API_URL, AIQUAA_API_BASE_URL, AIQUAA_ACCESS_TOKEN, CODEGRAPH_BIN, CODEGRAPH_ALLOWED_ROOTS, ENGRAM_BIN, ENGRAM_PROJECT_PREFIX, QA_CORE_ALLOWED_ROOTS, QA_CORE_MAX_FILE_SIZE, QA_CORE_LOG_LEVEL, QA_CORE_DRY_RUN. El código de dominio y de aplicación nunca lee process.env directamente — solo loadConfigFromEnvironment lo hace, y únicamente cuando lo llamas.

Desarrollo

npm install
npm run check   # typecheck + lint + test:coverage + build

Consulta CONTRIBUTING.md para el flujo de trabajo completo y SECURITY.md para el proceso de reporte de vulnerabilidades.

Licencia

MIT — consulta LICENSE.