Ctrl K
ComunidadGuías…

eval/assertions.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use strict";

const assert = require("assert");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { Parser, Language } = require("web-tree-sitter");
const createStructuralTools = require("../lib/structural-tools");

const GRAMMAR_PACKAGE = "@repomix/tree-sitter-wasms/out/tree-sitter-";
const LANGUAGES = Object.freeze({
  ".js": "javascript",
  ".jsx": "javascript",
  ".mjs": "javascript",
  ".cjs": "javascript",
  ".ts": "typescript",
  ".tsx": "tsx",
  ".py": "python",
  ".go": "go",
  ".rs": "rust",
});
const SKIP_DIRS = new Set([".git", "node_modules", "dist", "build", "coverage", "target"]);
let parserInit = null;
const languageCache = new Map();

function slash(value) {
  return String(value || "").replace(/\\/g, "/");
}

function sha256(value) {
  return crypto.createHash("sha256").update(value).digest("hex");
}

function resolveInside(root, relativePath) {
  const base = path.resolve(root);
  const target = path.resolve(base, String(relativePath || ""));
  const relative = path.relative(base, target);
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
    throw new Error("Fixture path escapes workspace: " + relativePath);
  }
  return target;
}

function writeFiles(workspace, files) {
  Object.keys(files || {}).sort().forEach((relativePath) => {
    const target = resolveInside(workspace, relativePath);
    fs.mkdirSync(path.dirname(target), { recursive: true });
    fs.writeFileSync(target, String(files[relativePath]), "utf8");
  });
}

function copyFixture(caseDefinition, workspace) {
  fs.mkdirSync(workspace, { recursive: true });
  writeFiles(workspace, caseDefinition.workspace || {});
  return workspace;
}

function materializeExpected(caseDefinition, workspace) {
  writeFiles(workspace, caseDefinition.expected || {});
}

function walkFiles(root) {
  const files = [];
  const queue = [path.resolve(root)];
  while (queue.length) {
    const current = queue.shift();
    let entries = [];
    try {
      entries = fs.readdirSync(current, { withFileTypes: true })
        .sort((a, b) => a.name.localeCompare(b.name));
    } catch (error) {
      continue;
    }
    entries.forEach((entry) => {
      const absolute = path.join(current, entry.name);
      if (entry.isDirectory()) {
        if (!SKIP_DIRS.has(entry.name)) queue.push(absolute);
      } else if (entry.isFile()) {
        files.push(slash(path.relative(root, absolute)));
      }
    });
  }
  return files.sort();
}

function snapshotWorkspace(workspace) {
  const snapshot = {};
  walkFiles(workspace).forEach((relativePath) => {
    const buffer = fs.readFileSync(resolveInside(workspace, relativePath));
    snapshot[relativePath] = {
      hash: sha256(buffer),
      bytes: buffer.length,
      content: buffer.toString("utf8"),
    };
  });
  return snapshot;
}

function changedFiles(before, after) {
  return Array.from(new Set(Object.keys(before || {}).concat(Object.keys(after || {}))))
    .filter((relativePath) => !before[relativePath] || !after[relativePath] ||
      before[relativePath].hash !== after[relativePath].hash)
    .sort();
}

async function language(languageName) {
  if (!parserInit) {
    parserInit = Parser.init({
      locateFile() {
        return require.resolve("web-tree-sitter/web-tree-sitter.wasm");
      },
    });
  }
  await parserInit;
  if (!languageCache.has(languageName)) {
    languageCache.set(languageName,
      Language.load(require.resolve(GRAMMAR_PACKAGE + languageName + ".wasm")));
  }
  return languageCache.get(languageName);
}

async function syntaxSnapshot(workspace) {
  const result = {};
  for (const relativePath of walkFiles(workspace)) {
    const languageName = LANGUAGES[path.extname(relativePath).toLowerCase()];
    if (!languageName) continue;
    const loadedLanguage = await language(languageName);
    const parser = new Parser();
    let tree = null;
    try {
      parser.setLanguage(loadedLanguage);
      tree = parser.parse(fs.readFileSync(resolveInside(workspace, relativePath), "utf8"));
      result[relativePath] = createStructuralTools._private.syntaxErrorCount(tree);
    } finally {
      if (tree) tree.delete();
      parser.delete();
    }
  }
  return result;
}

function syntaxRegressions(before, after) {
  const files = [];
  let count = 0;
  Object.keys(after || {}).sort().forEach((relativePath) => {
    const added = Math.max(0, Number(after[relativePath] || 0) - Number(before[relativePath] || 0));
    if (added) files.push({ path: relativePath, before: before[relativePath] || 0, after: after[relativePath], added });
    count += added;
  });
  return { count, files };
}

function loadCases(casesDirectory) {
  return fs.readdirSync(casesDirectory, { withFileTypes: true })
    .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
    .sort((a, b) => a.name.localeCompare(b.name))
    .map((entry) => {
      const file = path.join(casesDirectory, entry.name);
      const definition = JSON.parse(fs.readFileSync(file, "utf8"));
      definition.fixtureFile = file;
      if (!definition.id || !definition.prompt || !definition.workspace || !definition.expected) {
        throw new Error("Invalid eval fixture: " + entry.name);
      }
      return definition;
    });
}

async function evaluateCase(caseDefinition, options) {
  options = options || {};
  const workspace = path.resolve(options.workspace);
  const before = options.beforeSnapshot || {};
  const after = snapshotWorkspace(workspace);
  const changed = changedFiles(before, after);
  const checks = [];
  const failures = [];
  const record = (name, ok, detail) => {
    checks.push({ name, ok: !!ok, detail: detail || "" });
    if (!ok) failures.push(name + (detail ? ": " + detail : ""));
  };

  Object.keys(caseDefinition.expected || {}).sort().forEach((relativePath) => {
    const entry = after[relativePath];
    const expected = String(caseDefinition.expected[relativePath]);
    record("file:" + relativePath, !!entry && entry.content === expected,
      !entry ? "missing" : "content mismatch");
  });

  const rules = caseDefinition.assertions || {};
  const allowed = (rules.allowedChangedFiles || Object.keys(caseDefinition.expected || {})).slice().sort();
  const required = (rules.requiredChangedFiles || []).slice().sort();
  const unexpected = changed.filter((relativePath) => !allowed.includes(relativePath));
  const missingChanges = required.filter((relativePath) => !changed.includes(relativePath));
  record("diff-scope", unexpected.length === 0,
    unexpected.length ? "unexpected changes: " + unexpected.join(", ") : "");
  record("required-changes", missingChanges.length === 0,
    missingChanges.length ? "missing changes: " + missingChanges.join(", ") : "");

  (rules.protectedFiles || []).forEach((relativePath) => {
    const previous = before[relativePath];
    const current = after[relativePath];
    record("protected:" + relativePath, !!previous && !!current && previous.hash === current.hash,
      !previous ? "missing baseline" : !current ? "file removed" : "file changed");
  });

  const tools = createStructuralTools({ workspace });
  for (const expectedAst of rules.ast || []) {
    try {
      const result = await tools.ast_search.execute({
        path: expectedAst.path,
        kind: expectedAst.kind || "declaration",
        name: expectedAst.name || "",
        case_sensitive: expectedAst.caseSensitive !== false,
      });
      const matches = result.structuredContent.matches || [];
      const count = expectedAst.count == null ? 1 : expectedAst.count;
      let ok = matches.length === count;
      if (expectedAst.nodeType) ok = ok && matches.every((match) => match.node_type === expectedAst.nodeType);
      if (expectedAst.contains) ok = ok && matches.some((match) => match.text.includes(expectedAst.contains));
      if (expectedAst.notContains) ok = ok && matches.every((match) => !match.text.includes(expectedAst.notContains));
      record("ast:" + expectedAst.path + ":" + (expectedAst.name || expectedAst.kind), ok,
        "expected " + count + ", found " + matches.length);
    } catch (error) {
      record("ast:" + expectedAst.path, false, error.message || String(error));
    }
  }

  if (rules.verification && rules.verification.required) {
    const verification = options.verification || null;
    record("verification", !!verification && verification.passed === true,
      verification ? String(verification.status || "unknown") : "missing");
  }

  const beforeSyntax = options.beforeSyntax || {};
  const afterSyntax = await syntaxSnapshot(workspace);
  const regressions = syntaxRegressions(beforeSyntax, afterSyntax);
  record("syntax-regressions", regressions.count === 0,
    regressions.files.map((item) => item.path + " +" + item.added).join(", "));

  return {
    success: failures.length === 0,
    checks,
    failures,
    changedFiles: changed,
    syntaxRegressions: regressions.count,
    syntaxRegressionFiles: regressions.files,
    afterSnapshot: after,
    afterSyntax,
  };
}

async function assertCase(caseDefinition, options) {
  const result = await evaluateCase(caseDefinition, options);
  assert.strictEqual(result.success, true, result.failures.join("\n"));
  return result;
}

module.exports = {
  assertCase,
  changedFiles,
  copyFixture,
  evaluateCase,
  loadCases,
  materializeExpected,
  snapshotWorkspace,
  syntaxRegressions,
  syntaxSnapshot,
};